总结答案

Gin框架存储路由的数据结构是Radix Tree(基数树)。

原理

在这篇文章中,我们来深入探究Gin框架最核心的功能:路由树的构建原理。

前言

Gin框架的底层采用Radix Tree这种数据结构来存储路由的,在这篇文章中我们主要围绕以下几个问题来展开:

  • 什么是Radix Tree?
  • 使用Radix Tree有什么好处?
  • Gin框架如何基于Radix Tree构建路由树?

Radix Tree的定义

什么是Radix Tree

Radix Tree是一种数据结构,中文称为基数树或压缩前缀树,是一种优化的前缀树(字典树,Trie Tree)。

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图1

从上面的示意图可以看出,与前缀树相比,基数树的节点会在只有一个子节点情况下将子节点合并,因此更节省存储空间。

使用Radix Tree的好处在于,当要对有大量共同前缀的数据进行搜索时,可以快速查找而且节省空间。

因此Radix Tree常用IP查找,搜索引擎关联搜索,多级路由与路由分组存储与查找等。

Gin路由构建过程

HTTP协议规定了九种请求方法,分别为:

  1. ini
  2. 复制代码
  3. const (
  4. MethodGet = "GET"
  5. MethodHead = "HEAD"
  6. MethodPost = "POST"
  7. MethodPut = "PUT"
  8. MethodPatch = "PATCH"
  9. MethodDelete = "DELETE"
  10. MethodConnect = "CONNECT"
  11. MethodOptions = "OPTIONS"
  12. MethodTrace = "TRACE"
  13. )

Gin框架为每一种请求方法单独构建一棵路由树,New方法在初始化Engine对象时便会分配一个长度为9的数组trees来存储每种请求方法路由树:

  1. go
  2. 复制代码
  3. engine := &Engine{
  4. //省略其他代码
  5. trees: make(methodTrees, 0, 9),
  6. //省略其他代码
  7. }
  8. type methodTree struct {
  9. method string
  10. root *node
  11. }
  12. // 路由树数组,每个HTTP Method对应一个元素
  13. type methodTrees []methodTree

trees的类型为methodTrees,其元素类型为methodTree

methodTree中的root是一个路由树根节点的指针,其类型为nodenode节点是用于存储路由树节点的数据结构,node的结构如下:

  1. go
  2. 复制代码
  3. //节点
  4. type node struct {
  5. path string //节点路径
  6. indices string //子节点前缀列表,
  7. wildChild bool //是否包含通配符子节点
  8. nType nodeType //节点类型,static,root,param,catchAll
  9. priority uint32 //优先级
  10. children []*node //子节点数组
  11. handlers HandlersChain //路由处理函数
  12. fullPath string //完整路径
  13. }

其中nType表示节点类型,Gin框架将路由树节点分为四种类型:

  1. c
  2. 复制代码
  3. const (
  4. static nodeType = iota
  5. root
  6. param
  7. catchAll
  8. )

root类型表示根节点,整棵树只有一个根节点,static类型表示最普通的节点,如:

  1. sql
  2. 复制代码
  3. /order
  4. /user/list
  5. /user/add

param类型表示参数节点,如:

  1. bash
  2. 复制代码
  3. /user/:id

catchAll类型表示匹配所有路径的节点,如:

  1. bash
  2. 复制代码
  3. /user/*id

图解Gin路由构建过程

这里我们通过先通过示意图讲解Gin框架路由构建的过程,在示例中我们将添加以下的路由节点:

  1. bash
  2. 复制代码
  3. /user
  4. /user/list
  5. /user/delete
  6. /address/list
  7. /address/delete
  8. /admin/:id
  9. /admin/delete
  10. /admin/list/*name

我们前面提到了Gin有四种类型的路由节点,在下面的示意图中,我们用以下四种颜色来表示:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图2

添加第一个节/user时,该节点会被当作是路由树的根节点:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图3

添加第二个节点/user/list时,从根节点开始查找,待添加的节点与根节点有共同前缀/user,提取共同前缀后,/user节点下没有子节点,停止查找,创建子节点/list作为/user的子节点:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图4

添加第三个节点/user/delete时,该节点与根节点有共同前缀/user/user节点下有/list节点,去掉共同前缀后,将/delete/list节点比较,有共同前缀/,创建/节点作为/user的子节点,而listdelete作为/的子节点:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图5

添加/address/list节点时,该节点与根节点/user的共同前缀为/,因此将/提取出来作为根节点,创建address/list节点,把useraddress/list节点作为根节点/的子节点:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图6

添加/address/delete节点时,同样是从根节点一层层查找,与根节点/的共同前缀为/,继续往下查找,与address/list有共同前缀address/,将address/list分裂为address/listdelete节点,address/节点作为根节点的子节点,listdelete作为address/的子节点:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图7

添加/admin/:id节点,仍然是从根节点查找共同前缀,或分裂或创建节点,根据前面添加节点的经验,我们觉得结果应该是这样的:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图8

但是由于该节点包含有通配符:,因此会进一步分裂通配符节点,因此最终的结果为:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图9

包含通配符:节点虽然是第一个被添加的节点,但会放在最后一个节点,比如我们添加/admin/delete后,结果如下:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图10

添加/admin/list/*name节点时,对于包含通配符*的节点,Gin框架会在该节点前创建一个空白节点。

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图11

从代码层面理解Gin路由构建

接下来,我们再从代码层面来了解Gin是如何实现上述示意图所演示的路由构建逻辑的。

我们知道在Gin框架中,可以调用gin.Engine实例的GETPOST等方法添加路由,比如:

  1. css
  2. 复制代码
  3. engine := gin.New()
  4. engine.GET("/user/list")

而实际上添加路由的方法其底层最终都是调用了gin.EngineaddRoute方法,该方法与添加路由相关代码如下:

  1. go
  2. 复制代码
  3. func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  4. //省略其他代码
  5. //获取对应请求方法的根节点
  6. root := engine.trees.get(method)
  7. //根节点为空时,创建根节点
  8. if root == nil {
  9. root = new(node)
  10. root.fullPath = "/"
  11. engine.trees = append(engine.trees, methodTree{method: method, root: root})
  12. }
  13. //添加路由
  14. root.addRoute(path, handlers)
  15. //省略其他代码
  16. }

上面这段代码主要做了以下几件事:

  • 从数组methodTrees查找对应method的根节点rootroot的类型为node
  • 如果没有找到路由根节点,则以/为根路径创建一个根节点,并加入到engine.trees中。
  • 调用根节点root(类型为node)addRoute方法从树中寻找合适的位置创建路由节点。

接下来我们来看看nodeaddRoute方法,Gin框架添加路由的最主要逻辑就在这个方法中:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图12

nodeaddRoute的代码很长,其主要执行流程如下图所示:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图13

nodeinsertChild方法也是添加路由主要方法,其代码如下:

572.gin 框架怎么存储路由的是hash还是其它数据结构 - 图14

insertChild方法主要方法首先会判断所要添加的路径是否包含通配符,主要分三种情况:

  • 如果不包含通配符,则直接将路径添加到当前节点。
  • 包含通配符:时,创建子节点,如果后面有子路径,继续循环执行。
  • 包含通配符*时,创建一个空节点,创建一个通配符节点,将通配符节点添加到空节点后面。

小结

Gin框架底层采用Radix Tree作为路由树的存储结构,由于其能够快速处理大量具有共同前缀的数据,使得Gin能够为Web应用程序提供出色的性能和用户体验。通过使用Radix TreeGin能够快速、准确地找到匹配的路由,从而对HTTP请求进行相应的处理。