本文首发于同名公众号,欢迎关注~
上次我们说到 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 上启动服务}
平时开发中,用得比较多的就是 Get 和 Post 的方法,上面简单的写了个 demo,注册了两个路由及处理器,接下来跟着我一起一探究竟
注册路由
从官方文档和其他大牛的文章中可以知道,gin的路由是借鉴了 httprouter 实现的路由算法,所以得知 gin 的路由算法是基于前缀树这个数据结构的。
从 Get 方法进去看源码:
r.GET("/hello", func(context *gin.Context) { fmt.Fprint(context.Writer, "hello world") })
会来到 routergroup.go 的 Get 函数,可以发现方法的承载者已经是 *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 的数据结构的时候,已经提到了 nType、maxParams、wildChild这三个字段与动态路由的设计实现有关的,下面就是关于路由注册时如果是动态路由时的处理:
// tree.gofunc (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 里的值,再往深处走,Params 和 ByName 其实来自 tree.go:
// context.gotype Context struct { …… Params Params ……}// tree.gotype 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.gotype nodeValue struct { handlers HandlersChain params Params tsr bool fullPath string}// getValue 返回的 nodeValue 的结构,里面包含处理好的 Paramsfunc (n *node) getValue(path string, po Params, unescape bool) (value nodeValue) { value.params = powalk: // 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 } …… } }}
讲到这里就已经对路由注册和动态路由的实现流程和原理分析得差不多了,画一个核心流程图总结一下:
路由组
gin 用 RouterGroup 路由组包住了路由实现了路由分组功能。之前说到 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.gofunc (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { // 拼接获取绝对路径 absolutePath := group.calculateAbsolutePath(relativePath) // 合并路由处理器集合 handlers = group.combineHandlers(handlers) ……}
参考链接:
1)https://segmentfault.com/a/1190000016655709
2)https://blog.csdn.net/u013949069/article/details/78056102