内容简介:上次我们说到 [gin 的启动过程及实现](),今天来细讲 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 上启动服务 }
平时开发中,用得比较多的就是 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.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
里的值,再往深处走, Params
和 ByName
其实来自 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 } …… } } }
讲到这里就已经对路由注册和动态路由的实现流程和原理分析得差不多了,画一个核心流程图总结一下:
路由组
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.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 知识
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- express源码分析-路由
- flask 源码解析3:路由
- RocketMQ源码分析之路由中心
- Laravel HTTP——添加路由源码分析
- RocketMQ 源码分析之路由中心(NameServer)
- 网关 Spring-Cloud-Gateway 源码解析 —— 路由(2.2)之 RouteDefinitionRouteLocator 路由配置
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
编程算法新手自学手册
管西京 / 机械工业 / 2012-1 / 69.80元
《编程算法新手自学手册》主要内容简介:算法是指在有限步骤内求解某一问题所使用的一组定义明确的规则。程序员都会看重数据结构和算法的作用,水平越高,就越能理解算法的重要性。算法不仅是运算工具,更是程序的灵魂。《编程算法新手自学手册》循序渐进、由浅入深地详细讲解了基于C语言算法的核心技术,并通过具体实例的实现过程演练了各个知识点的具体使用流程。全书共11章,分为4篇。1~2章是基础篇,介绍算法开发所必需......一起来看看 《编程算法新手自学手册》 这本书的介绍吧!
XML、JSON 在线转换
在线XML、JSON转换工具
正则表达式在线测试
正则表达式在线测试