本文基于gin 1.1 源码解读
1. 注册路由
我们先来看一段gin代码,来看看最终得到的一颗路由树长啥样
go
func TestGinDocExp(t *testing.T) {
engine := gin.Default()
engine.GET("/api/user", func(context *gin.Context) {
fmt.Println("api user")
})
engine.GET("/api/user/info/a", func(context *gin.Context) {
fmt.Println("api user info a")
})
engine.GET("/api/user/information", func(context *gin.Context) {
fmt.Println("api user information")
})
engine.Run()
}
看起来像是一颗前缀树,我们后面再仔细深入源码
1.1 gin.Default
gin.Default()
返回了一个Engine
结构体指针,同时添加了2个函数,Logger
和 Recovery
。
go
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
New
方法中初始化了 Engine
,同时还初始化了一个RouterGroup
结构体,并将Engine
赋值给RouterGroup
,相当于互相套用了
go
func New() *Engine {
engine := &Engine{
RouterGroup: RouterGroup{
Handlers: nil, // 业务handle,也可以是中间件
basePath: "/", // 根地址
root: true, // 根路由
},
RedirectTrailingSlash: true,
RedirectFixedPath: false,
HandleMethodNotAllowed: false,
ForwardedByClientIP: true,
trees: make(methodTrees, 0, 9), // 路由树
}
engine.RouterGroup.engine = engine
// 这里使用pool来池化Context,主要目的是减少频繁创建和销毁对象带来的内存分配和垃圾回收的开销
engine.pool.New = func() interface{} {
return engine.allocateContext()
}
return engine
}
在执行完gin.Defualt 后,gin的内容,里面已经默认初始化了2个handles
1.2 Engine.Get
在Get方法内部,最终都是调用到了group.handle
方法,包括其他的POST,DELETE等
- group.handle
go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 获取绝对路径, 将group中的地址和当前地址进行组合
absolutePath := group.calculateAbsolutePath(relativePath)
// 将group中的handles(Logger和Recovery)和当前的handles合并
handlers = group.combineHandlers(handlers)
// 核心在这里,将handles添加到路由树中
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
- group.engine.addRoute
plain
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// 通过遍历trees中的内容,判断是否在这之前,同一个http方法下已经添加过路由
// trees 是一个[]methodTree 切片,有2个字段,method 表示方法,root 表示当前节点,是一个node结构体
root := engine.trees.get(method)
// 如果这是第一个路由则创建一个新的节点
if root == nil {
root = new(node)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
}
- root.addRoute(path, handlers)
这里的代码比较多,其实大家可以简单的认为就是将handle
和path
进行判断,注意这里的path
不是一个完整的注册api,而是去掉了公共前缀后的那部分字符串。
go
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path // 存储当前节点的完整路径
n.priority++ // 优先级自增1
numParams := countParams(path) // 统计当前节点的动态参数个数
// non-empty tree,非空路由树
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// Update maxParams of the current node
// 统计当前节点及子节点中最大数量的参数个数
// maxParams 可以快速判断当前节点及其子树是否能匹配包含一定数量参数的路径,从而加速匹配过程。(GPT)
if numParams > n.maxParams {
n.maxParams = numParams
}
// Find the longest common prefix.
// This also implies that the common prefix contains no ':' or '*'
// since the existing key can't contain those chars.
// 计算最长公共前缀
i := 0
max := min(len(path), len(n.path))
for i < max && path[i] == n.path[i] {
i++
}
// Split edge,当前路径和节点路径只有部分重叠,且存在分歧
if i < len(n.path) {
// 将后缀部分创建一个新的节点,作为当前节点的子节点。
child := node{
path: n.path[i:],
wildChild: n.wildChild,
indices: n.indices,
children: n.children,
handlers: n.handlers,
priority: n.priority - 1,
}
// Update maxParams (max of all children)
// 路径分裂时,确保当前节点及子节点中是有最大的maxParams
for i := range child.children {
if child.children[i].maxParams > child.maxParams {
child.maxParams = child.children[i].maxParams
}
}
n.children = []*node{&child}
// []byte for proper unicode char conversion, see #65
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handlers = nil
n.wildChild = false
}
// Make new node a child of this node
if i < len(path) {
path = path[i:] // 获取当前节点中非公共前缀的部分
// 当前节点是一个动态路径
// TODO 还需要好好研究一下
if n.wildChild {
n = n.children[0]
n.priority++
// Update maxParams of the child node
if numParams > n.maxParams {
n.maxParams = numParams
}
numParams--
// Check if the wildcard matches
// 确保新路径的动态部分与已有动态路径不会冲突。
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// check for longer wildcard, e.g. :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
}
panic("path segment '" + path +
"' conflicts with existing wildcard '" + n.path +
"' in path '" + fullPath + "'")
}
// 获取非公共前缀部分的第一个字符
c := path[0]
// slash after param
// 当前节点是动态参数节点,且最后一个字符时/,同时当前节点还只有一个字节
if n.nType == param && c == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue walk
}
// Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// Otherwise insert it
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
n.indices += string([]byte{c})
child := &node{
maxParams: numParams,
}
n.children = append(n.children, child)
// 增加当前子节点的优先级
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(numParams, path, fullPath, handlers)
return
} else if i == len(path) { // Make node a (in-path) leaf
if n.handlers != nil {
panic("handlers are already registered for path ''" + fullPath + "'")
}
n.handlers = handlers
}
return
}
} else { // Empty tree
n.insertChild(numParams, path, fullPath, handlers)
n.nType = root
}
}
1.3 Engine.Run
go
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
// 处理一下web的监听地址
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
// 最终还是使用http来启动了一个web服务
err = http.ListenAndServe(address, engine)
return
}
2. 路由查找
在上一篇文章中介绍了,http 的web部分的实现,http.ListenAndServe(address, engine)
在接收到请求后,最终会调用engine
的ServeHTTP
方法
go
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从context池中获取一个Context
c := engine.pool.Get().(*Context)
// 对Context进行一些初始值操作,比如赋值w和req
c.writermem.reset(w)
c.Request = req
c.reset()
// 最终进入这个方法来处理请求
engine.handleHTTPRequest(c)
// 处理结束后将Conetxt放回池中,供下一次使用
engine.pool.Put(c)
}
2.1 engine.handleHTTPRequest()
go
func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method // 当前客户端请求的http 方法
path := context.Request.URL.Path // 查询客户端请求的完整请求地址
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod {
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(path, context.Params)
if handlers != nil {
context.handlers = handlers // 所有的handles 请求对象
context.Params = params // 路径参数,例如/api/user/:id , 此时id就是一个路径参数
context.Next() // 执行所有的handles方法
context.writermem.WriteHeaderNow()
return
}
}
}
// 这里客户端请求的地址没有匹配上,同时检测请求的方法有没有注册,若没有注册过则提供请求方法错误
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method != httpMethod {
if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
context.handlers = engine.allNoMethod
serveError(context, 405, default405Body)
return
}
}
}
}
// 路由地址没有找到
context.handlers = engine.allNoRoute
serveError(context, 404, default404Body)
}
2.2 context.Next()
这个方法在注册中间件的使用会使用的较为频繁
go
func (c *Context) Next() {
// 初始化的时候index 是 -1
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
c.handlers[c.index](c) // 依次执行注册的handles
}
}
我们来看一段gin 是执行中间件的流程
go
func TestGinMdls(t *testing.T) {
engine := gin.Default()
engine.Use(func(ctx *gin.Context) {
fmt.Println("请求过来了")
// 这里可以做一些横向操作,比如处理用户身份,cors等
ctx.Next()
fmt.Println("返回响应")
})
engine.GET("/index", func(context *gin.Context) {
fmt.Println("index")
})
engine.Run()
}
curl http://127.0.0.1:8080/index
通过响应结果我们可以分析出,请求过来时,先执行了Use中注册的中间件,然后用户调用ctx.Next() 可以执行下一个handle,也就是用户注册的/index方法的handle