gin 源码阅读(二)– 路由和路由组

上次我们说到 [gin 的启动过程及实现](),今天来细讲 gin 的路由。

用法

还是老样子,先从使用方式开始:

func main() {
    r := gin.Default()

    r.GET("/hello", func(context *gin.Context) {
        fmt.Fprint(context.Writer, "hello world")
    })

    r.POST("/somePost", func(context *gin.Context) {
        context.String(http.StatusOK, "some post")
    })

    r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

平时开发中,用得比较多的就是 GetPost 的方法,上面简单的写了个 demo,注册了两个路由及处理器,接下来跟着我一起一探究竟

注册路由

从官方文档和其他大牛的文章中可以知道, gin 的路由是借鉴了 httprouter 实现的路由算法,所以得知 gin 的路由算法是基于 前缀树 这个数据结构的。

Get 方法进去看源码:

r.GET("/hello", func(context *gin.Context) {
        fmt.Fprint(context.Writer, "hello world")
    })

会来到 routergroup.goGet 函数,可以发现方法的承载者已经是 *RouterGroup

// GET is a shortcut for router.Handle("GET", path, handle).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle("GET", relativePath, handlers)
}

从注释中我们可以看到 GET is a shortcut for router.Handle("GET", path, handle)

也就是说 GET 方法的注册也可以等价于:

helloHandler := func(context *gin.Context) {
            fmt.Fprint(context.Writer, "hello world")
        }

    r.Handle("GET", "/hello", helloHandler)

再来看一下 Handle 方法的具体实现:

func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) IRoutes {
    if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil {
        panic("http method " + httpMethod + " is not valid")
    }
    return group.handle(httpMethod, relativePath, handlers)
}

不难发现,无论是 r.GET 还是 r.Handle 最终都是指向了 group.handle

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 计算绝对路径,这是因为可能会有路由组会在外层包裹的原因
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 联合路由组的 handler 和新注册的 handler
    handlers = group.combineHandlers(handlers)
    // 注册路由的真正入口
    group.engine.addRoute(httpMethod, absolutePath, handlers)
    // 返回 IRouter 接口对象,这个放在路由组进行分析
    return group.returnObj()
}

接下来又回到了 gin.go ,可以看到上面的注册入口是通过 group.engine 调用的,大家不用看 routerGroup 的结构也大致猜出来了吧,其实 engine 才是真正的路由树 router ,而 gin 为了实现路由组的功能,所以在外面又包了一层 routerGroup ,实现路由分组,路由路径组合隔离的功能。

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    // 基础校验
    assert1(path[0] == '/', "path must begin with '/'")
    assert1(method != "", "HTTP method can not be empty")
    assert1(len(handlers) > 0, "there must be at least one handler")
    
    debugPrintRoute(method, path, handlers)
    // 每个httpMethod都拥有自己的一颗树
    root := engine.trees.get(method)
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    // 在路由树中添加路径及请求处理handler
    root.addRoute(path, handlers)
}

以上就是注册路由的过程,整体流程其实挺清晰的。

路由树

终于来到了关键的实现路由树的地方 tree.go

先来看看 tree 的结构:

type methodTree struct {
    method string
    root   *node
}

type methodTrees []methodTree

上面的 engine.trees.get(method) 就是遍历这个以 httpMethod 分隔的数组:

func (trees methodTrees) get(method string) *node {
    for _, tree := range trees {
        if tree.method == method {
            return tree.root
        }
    }
    return nil
}

关键在于 node

type node struct {
    path      string // 当前节点相对路径(与祖先节点的 path 拼接可得到完整路径)
    indices   string // 所有孩子节点的path[0]组成的字符串
    children  []*node // 孩子节点
    handlers  HandlersChain // 当前节点的处理函数(包括中间件)
    priority  uint32 // 当前节点及子孙节点的实际路由数量
    nType     nodeType // 节点类型
    maxParams uint8 // 子孙节点的最大参数数量
    wildChild bool // 孩子节点是否有通配符(wildcard)
    fullPath  string // 路由全路径
}

nType 有这几个值:

const (
    static nodeType = iota // 普通节点,默认
    root // 根节点
    param // 参数路由,比如 /user/:id
    catchAll // 匹配所有内容的路由,比如 /article/*key
)

下面的 addRoute 方法就是对这棵前缀树的构建过程,实际上就是不断寻找最长前缀的过程。

func (n *node) addRoute(path string, handlers HandlersChain) {
    ……
    // non-empty tree
    if len(n.path) > 0 || len(n.children) > 0 {
    walk:
            ……

            // Make new node a child of this node
            if i < len(path) {
                ……
                c := path[0]
                // 一系列的判断与校验
                ……
                // Otherwise insert it
                if c != ':' && c != '*' {
                    // []byte for proper unicode char conversion, see #65
                    n.indices += string([]byte{c})
                    child := &node{
                        maxParams: numParams,
                        fullPath:  fullPath,
                    }
                    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
    }
}

最后画一下 gin 构建前缀树的示意图:

r.GET("/", func(context *gin.Context) {})
r.GET("/test", func(context *gin.Context) {})
r.GET("/te/n", func(context *gin.Context) {})
r.GET("/pass", func(context *gin.Context) {})
r.GET("/part/:id", func(context *gin.Context) {})
r.GET("/part/:id/pen", func(context *gin.Context) {})

动态路由

在画前缀树的时候,写到一个了路由 /part/:id ,这里的 :id 就是动态路由了,可以根据路由中指定的参数来解析 url 中对应动态路由里的参数值。

其实在说到 node 的数据结构的时候,已经提到了 nTypemaxParamswildChild 这三个字段与动态路由的设计实现有关的,下面就是关于路由注册时如果是动态路由时的处理:

// tree.go
func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) {
        ……
    
        if c == ':' { // param
            // 在通配符开头拆分路径
            if i > 0 {
                n.path = path[offset:i]
                offset = i
            }

            child := &node{
                nType:     param,
                maxParams: numParams,
                fullPath:  fullPath,
            }
            n.children = []*node{child}
            // 如果孩子节点是参数路由,就会将本节点wildChild设置为true
            n.wildChild = true
            n = child
            n.priority++
            numParams--

            // 如果路径没有以通配符结尾,则将有另一个以"/" 开头的非通配符子路径
            // 可以理解为后面还有节点
            if end < max {
                n.path = path[offset:end]
                offset = end

                child := &node{
                    maxParams: numParams,
                    priority:  1,
                    fullPath:  fullPath,
                }
                n.children = []*node{child}
                n = child
            }

        } else { // catchAll
            ……

            n.path = path[offset:i]

            // 匹配所有内容的通配符 如 /*key
            
            // first node: catchAll node with empty path
            child := &node{
                wildChild: true,
                nType:     catchAll,
                maxParams: 1,
                fullPath:  fullPath,
            }
            n.children = []*node{child}
            n.indices = string(path[i])
            // 在这里将 node 进行赋值了
            n = child
            n.priority++

            // second node: node holding the variable
            child = &node{
                path:      path[i:],
                nType:     catchAll,
                maxParams: 1,
                handlers:  handlers,
                priority:  1,
                fullPath:  fullPath,
            }
            n.children = []*node{child}

            return
        }
    }

    // insert remaining path part and handle to the leaf
    n.path = path[offset:]
    n.handlers = handlers
    n.fullPath = fullPath
}

我们知道 gin 框架中对于动态路由参数接收时是用 context.Param(key string) 的,下面跟着一个简单的 demo 来做

helloHandler := func(context *gin.Context) {
        name := context.Param("name")
        fmt.Fprint(context.Writer, name)
    }

r.Handle("GET", "/hello/:name", helloHandler)

来看下 Param 写了啥:

// Param returns the value of the URL param.
// It is a shortcut for c.Params.ByName(key)
//     router.GET("/user/:id", func(c *gin.Context) {
//         // a GET request to /user/john
//         id := c.Param("id") // id == "john"
//     })
func (c *Context) Param(key string) string {
    return c.Params.ByName(key)
}

看注释,其实写得已经很明白了,这个函数会返回动态路由中关于参数在请求 url 里的值,再往深处走, ParamsByName 其实来自 tree.go

// context.go
type Context struct {
    ……

    Params   Params
    ……
}

// tree.go
type Param struct {
    Key   string
    Value string
}

// Params 是有个有序的 Param 切片,路由中的第一个参数会对应切片的第一个索引
type Params []Param

// 遍历 Params 获取值
func (ps Params) Get(name string) (string, bool) {
    for _, entry := range ps {
        if entry.Key == name {
            return entry.Value, true
        }
    }
    return "", false
}

// 封装了一下,调用上面的 Get 方法
func (ps Params) ByName(name string) (va string) {
    va, _ = ps.Get(name)
    return
}

获取参数 key 的地方找到了,那从路由里拆解并设置 Params 的地方呢?

// tree.go

type nodeValue struct {
    handlers HandlersChain
    params   Params
    tsr      bool
    fullPath string
}

// getValue 返回的 nodeValue 的结构,里面包含处理好的 Params
func (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) {
    value.params = po
walk: // Outer loop for walking the tree
    for {
        if len(path) > len(n.path) {
            if path[:len(n.path)] == n.path {
                path = path[len(n.path):]
                // 如果这个节点没有通配符,就进行往孩子节点遍历
                if !n.wildChild {
                    c := path[0]
                    for i := 0; i < len(n.indices); i++ {
                        if c == n.indices[i] {
                            n = n.children[i]
                            continue walk
                        }
                    }

                    // 如果没找到有通配符标识的节点,直接重定向到该 url
                    value.tsr = path == "/" && n.handlers != nil
                    return
                }

                // handle wildcard child
                n = n.children[0]
                switch n.nType {
                    //可以看到这里是用 nType 来判断的
                case param:
                    // find param end (either '/' or path end)
                    end := 0
                    for end < len(path) && path[end] != '/' {
                        end++
                    }

                    // 遍历 url 获取参数对应的值
                    
                    // save param value
                    if cap(value.params) < int(n.maxParams) {
                        value.params = make(Params, 0, n.maxParams)
                    }
                    i := len(value.params)
                    value.params = value.params[:i+1] // expand slice within preallocated capacity
                    value.params[i].Key = n.path[1:] // 除去 ":",如 :id -> id
                    val := path[:end]
                    // url 编码解析以及 params 赋值
                    if unescape {
                        var err error
                        if value.params[i].Value, err = url.QueryUnescape(val); err != nil {
                            value.params[i].Value = val // fallback, in case of error
                        }
                    } else {
                        value.params[i].Value = val
                    }
                ……
        }
    }
}

讲到这里就已经对路由注册和动态路由的实现流程和原理分析得差不多了,画一个核心流程图总结一下:

路由组

ginRouterGroup 路由组包住了路由实现了路由分组功能。之前说到 engine 的时候说到 engine 的结构中是组合了 RouterGroup 的,而 RouterGroup 中其实也包含了 engine

type RouterGroup struct {
    Handlers HandlersChain
    basePath string
    engine   *Engine
    root     bool
}

type Engine struct {
  RouterGroup
  ...
}

这样的做法让 engine 直接拥有了管理路由的能力,也就是 engine.GET(xxx) 可以直接注册路由的来由。而 RouterGroup 中包含了 engine 的指针,这样实现了 engine 的单例,这个也是比较巧妙的做法之一。

不仅如此, RouterGroup 实现了 IRouter 接口,接口中的方法都是通过调用 engine.addRoute()` 将handler链接到路由树中:

var _ IRouter = &RouterGroup{}

type IRouter interface {
    IRoutes
    Group(string, ...HandlerFunc) *RouterGroup
}

type IRoutes interface {
    Use(...HandlerFunc) IRoutes

    Handle(string, string, ...HandlerFunc) IRoutes
    Any(string, ...HandlerFunc) IRoutes
    GET(string, ...HandlerFunc) IRoutes
    POST(string, ...HandlerFunc) IRoutes
    DELETE(string, ...HandlerFunc) IRoutes
    PATCH(string, ...HandlerFunc) IRoutes
    PUT(string, ...HandlerFunc) IRoutes
    OPTIONS(string, ...HandlerFunc) IRoutes
    HEAD(string, ...HandlerFunc) IRoutes

    StaticFile(string, string) IRoutes
    Static(string, string) IRoutes
    StaticFS(string, http.FileSystem) IRoutes
}

路由组的功能显而易见,就是让路由分组管理,在组内的路由的前缀都统一加上组路由的路径,看下 demo

router := gin.Default()

v1 := router.Group("/v1")
{
    v1.POST("/hello", helloworld) // /v1/hello
    v1.POST("/hello2", helloworld2) // /v1/hello2
}

包住路由并在注册路由时进行拼接的地方是在注册路由的函数中:

// routergroup.go
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    // 拼接获取绝对路径
    absolutePath := group.calculateAbsolutePath(relativePath)
    // 合并路由处理器集合
    handlers = group.combineHandlers(handlers)
    ……
}

参考链接:

1) https://segmentfault.com/a/11...

2) https://blog.csdn.net/u013949...

欢迎关注我们的微信公众号,每天学习Go知识