一、概览
Gin 是在 Golang HTTP 标准库 net/http 基础之上的再封装,两者的交互边界如下图:
首先看一下gin的简单使用:
func main() {
e := gin.Default()
e.GET("/ping", func(context *gin.Context) {
context.JSON(200, "pong")
})
e.Run("127.0.0.1:8080")
}
Run()方法底层实际调用的是还是 net/http的ListenAndServe(addr, handler)
func (engine *Engine) Run(addr ...string) (err error) {
// ... 省略代码
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine) 《=====底层还是http.ListenAndServe()
return
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
二、客户端的request数据如何流入gin
因为 Run() 方法调用的是 http.ListenAndServe ,所以 gin 建立 socket 的过程,accept 请求的过程与 net/http 没有差别,net/http 时如何建立socket和accept的,看我的上一篇文章 net/http 是如何处理请求的。
唯一不同的就是在获取 ServeHTTP 的位置:
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
// Handler 通过 http.ListenAndServe() 进行赋值
// Gin框架通过给Handler赋值为engine (err = http.ListenAndServe(address, engine))
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req) 《=====此处开始和 net/http 中调用的ServeHTTP()有区别
}
gin 的 ServeHTTP:
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//1.从 临时对象池 取一块儿内存
c := engine.pool.Get().(*Context)
//2.初始化内存
c.writermem.reset(w)
//3.
c.Request = req
c.reset()
//4.处理请求
engine.handleHTTPRequest(c)
//5.归还内存
engine.pool.Put(c)
}
此方法 ServeHTTP 是 gin 的第一步,将请求转入 gin 的处理流程。
流程图:

三. gin的路由是如何实现的
1. 路由的设计
- 在 Gin 框架中,路由规则被分成了最多 9 棵前缀树,每一个 HTTP Method对应一棵「前缀树」;
- 树的节点按照 URL 中的 / 符号进行层级划分;
- URL 支持 :name 形式的名称匹配,还支持 *subpath 形式的路径通配符;
- 每个节点都会挂接若干请求处理函数构成一个请求处理链 HandlersChain。当一个请求到来时,在这棵树上找到请求 URL 对应的节点,拿到对应的请求处理链来执行就完成了请求的处理。
9个方法分别对应一颗前缀树:
GET、POST、HEAD、PUT、PATCH、DELETE、CONNECT、OPTIONS、TRACE
树的节点按照 URL 中的 / 符号进行层级划分,例如,路由的地址是:
- /hi
- /hello
- /:name/:id
那么gin对应的树会是这个样子的:

节点的数据结构如下:
type node struct {
path string
indices string
wildChild bool
nType nodeType
priority uint32
children []*node // child nodes, at most 1 :param style node at the end of the array
handlers HandlersChain
fullPath string
}
关于前缀树,参考这篇: Radix树 和这篇 gin 框架之路由前缀树初始化分析
路由匹配规则:
路由参数:
是指在路径中定义的参数,例如 /user/42必选参数:
使用 :param 的语法完成必选参数,例如 /user/:ID ,可以匹配 /user/42 ,但不能匹配 > /user 或 /user/可选参数:
使用 *param 的语法完成可选参数,例如 /user/*ID ,可匹配 /user/42 和 /user/
2. 路由的两种注册
- 普通注册
e := gin.Default()
e.GET("/ping", func(context *gin.Context) {
context.JSON(200, "pong")
})
- 使用 RouteGroup注册 ,便于版本维护
e := gin.Default()
v1 := e.Group("v1")
{
v1.POST("/v1/test", func(context *gin.Context) {
context.JSON(200, "message")
})
}
- 使用中间件
e.Use(gin.Recovery())
3. 生成路由树&注册添加路由 的具体实现
从e.GET、e.POST方法入口,逐级调用到
group.engine.addRoute(httpMethod, absolutePath, handlers)
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
统一调用 RouterGroup 的 handle() :
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// ... 省略代码
group.engine.addRoute(httpMethod, absolutePath, handlers) 《=====
// ... 省略代码
}
addRoute 添加路由,通过method 获取对应的路由树,然后注册添加路由
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// ... 省略代码
root := engine.trees.get(method) 《=====获取对应方法的前缀树
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers) 《===== 添加路由处理函数
// ... 省略代码
}
结构体 node 是路由树的节点:
type node struct {
path string //当前叶子节点的最长的前缀
indices string //通常维护了children列表的path的各首字符组成的string
wildChild bool //默认是false,当children时通配符类型时,wildChild为true
nType nodeType //是节点的类型,默认是static类型,还包括root类型,对于path包含:通配符,类型是param,对于包含*通配符,类型是catchAll
priority uint32 //代表了有几条路会经过此节点,用于在节点进行排序时使用
children []*node //就是一颗树的叶子结点。每个路由的去掉前缀后,都被分布在这些 children 数组里
handlers HandlersChain //存放的是当前叶子节点对应的路由的处理函数
fullPath string //是从root节点到当前节点的全部path部分,如果此节点为终节点,handlers为对硬的处理链,非则为nil。maxParams是当前节点到各个叶子节点的包含的通配符的最大数量
}
其实 gin 的实现不像一个真正的树, children []*node 所有的孩子都放在这个数组里面, 利用indices, priority变相实现一棵树。
###4. server 端收到客户端请求时,如何找到对应的路由的handler?
前面分析到,在Gin框架中,通过Run() 方法调用了 http.ListenAndServe 进行监听和创建新连接处理请求 ,最终会调用到 serveHTTP。
serveHTTP 中调用了 engine.handleHTTPRequest(c) ,这个方法第一件事就是去路由树里面去匹配对应的URL,找到相关路由,拿到相关的处理函数。
func (engine *Engine) handleHTTPRequest(c *Context) {
// ... 省略代码
//0.一棵前缀树
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
//1.去路由树找匹配的url
value := root.getValue(rPath, c.Params, unescape) <====从路由树中查找路由
//2.拿到相关处理函数
if value.handlers != nil {
c.handlers = value.handlers 《======处理函数赋值给context
c.Params = value.params
c.fullPath = value.fullPath
c.Next() 《======调用处理函数
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
// ... 省略代码
}
在路由树中查找路由 node.getValue:
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
// ... 代码省略
}
Next 这个方法里调用路由处理函数,如果有中间件,它会依次进行调用,最后在调用业务处理函数。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}