上一篇讲的是gin 框架的启动原理,今天来讲一下 gin 路由的实现。

1

用法

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

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,注册了两个路由及处理器,接下来跟着我一起一探究竟。

2

注册路由

从官方文档和其他大牛的文章中可以知道,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)}

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

3

路由树

终于来到了关键的实现路由树的地方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) {})

4

动态路由

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

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

// 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 里的值,再往深处走,ParamsByName 其实来自 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.go 里的 getValue 方法:

// 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          }        ……        }  }}

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

5

路由组

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.gofunc (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {    // 拼接获取绝对路径  absolutePath := group.calculateAbsolutePath(relativePath)    // 合并路由处理器集合  handlers = group.combineHandlers(handlers)  ……}

关于 gin 的路由、路由组源码分析就到这里了,下一篇将会写 gin 的中间件源码分析,敬请期待~

最后的最后,如果觉得本文写得还可以,就点个“在看”鼓励一下我吧

参考资料:

1.https://segmentfault.com/a/1190000016655709

2.https://blog.csdn.net/u013949069/article/details/78056102

推荐阅读

  • gin 源码阅读(一)-- 启动

  • Go框架解析-Gin


喜欢本文的朋友,欢迎关注“Go语言中文网”:

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

gin context和官方context_gin 源码阅读(二) 路由和路由组相关推荐

  1. gin context和官方context_gin 源码阅读(一) -- 启动

    文章首发于同名公众号,欢迎关注~ 因为 gin 的安装教程已经到处都有了,所以这里省略如何安装, 建议直接去 github 官方地址的 README 中浏览安装步骤,顺便了解 gin 框架的功能.ht ...

  2. mybatis源码阅读(二):mybatis初始化上

    转载自  mybatis源码阅读(二):mybatis初始化上 1.初始化入口 //Mybatis 通过SqlSessionFactory获取SqlSession, 然后才能通过SqlSession与 ...

  3. Soul网关源码阅读(八)路由匹配初探

    Soul网关源码阅读(八)路由匹配初探 简介      今日看看路由的匹配相关代码,查看HTTP的DividePlugin匹配 示例运行      使用HTTP的示例,运行Soul-Admin,Sou ...

  4. LeGo-LOAM激光雷达定位算法源码阅读(二)

    文章目录 1.featureAssociation框架 1.1节点代码主体 1.2 FeatureAssociation构造函数 1.3 runFeatureAssociation()主体函数 2.重 ...

  5. gin context和官方context_Gin框架系列01:极速上手

    Gin是什么? Gin是Go语言编写的web框架,具备中间件.崩溃处理.JSON验证.内置渲染等多种功能. 准备工作 本系列演示所有代码都在Github中,感兴趣的同学可以自行查阅,欢迎大家一起完善. ...

  6. nginx源码阅读(二).初始化:main函数及ngx_init_cycle函数

    前言 在分析源码时,我们可以先把握主干,然后其他部分再挨个分析就行了.接下来我们先看看nginx的main函数干了些什么. main函数 这里先介绍一些下面会遇到的变量类型: ngx_int_t: t ...

  7. DBFace: 源码阅读(二)

    上篇链接 看LZ上篇博客的时间竟然是7月18日,着实是懈怠了,其实有很多东西需要总结归纳,这周末就补一下之前欠的债吧 上篇主要介绍了DBFace的大体框架,这篇主要介绍数据的预处理部分 5. 数据预处 ...

  8. Struts2源码阅读(二)_ActionContext及CleanUP Filter

    1. ActionContext ActionContext是被存放在当前线程中的,获取ActionContext也是从ThreadLocal中获取的.所以在执行拦截器. action和result的 ...

  9. Mybatis源码阅读(二)

    本文主要介绍Java中,不使用XML和使用XML构建SqlSessionFactory,通过SqlSessionFactory 中获取SqlSession的方法,使用SqlsessionManager ...

最新文章

  1. JZOJ__Day 6:【普及模拟】Oliver的成绩(score)
  2. 观察者模式的应用场景
  3. 在VMWare上安装Win3.2
  4. 服务器lunix系统开启多用户,Ubuntu 服务器设置软件多用户访问
  5. 2018.8.2 Juint测试介绍及其命名的规范
  6. Cloud一分钟 | 苹果更新“隐私页面”;中国联通大数据正式升级,进入数智新阶段...
  7. debian 安装java_debian9安装jdk1.8
  8. 第五章应用系统安全基础备考要点及真题分布
  9. CN笔记:第三章 链路层
  10. MATLAB music分解信号,MUSIC算法信号频率问题求解
  11. YUV RGB 常见视频格式解析
  12. GCC 预处理的宏 (predefined macros)
  13. 人无信则不立,您了解自己的信用情况吗?
  14. 《精读 Mastering ABP Framework》教程发布
  15. 聊城市普通话水平测试软件音频,聊城市普通话水平测试培训-聊城市教师教育网.ppt...
  16. WOT讲师杨钊:人工智能将在不同应用场景逐步落地
  17. Git 如何把master的内容更新到分支
  18. 强烈建议 | 转行Python最好看一下这篇文章
  19. 贝塞尔方程与贝塞尔函数
  20. 关注Android,关注Android相关论坛。

热门文章

  1. 使用NSURLConnection实现大文件断点下载
  2. Java C# 加密解密类库
  3. Geoserver汉语版出来啦!!
  4. 第一记: JS变量类型判断(VUE源码解读)
  5. 深度学习目标检测(object detection)系列(一) R-CNN
  6. 各类商会协会单位类织梦模板(带手机端)
  7. java设计模式之装饰器模式
  8. thinkphp如何一次性的上传多个文件,在文件域中可以多选?
  9. 【不同的Java垃圾回收器的比较】
  10. centos安装nginx小记