Gin框架:路由树底层构建原理

公众号:程序员读书,欢迎关注

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

前言

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

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

Radix Tree的定义

什么是Radix Tree

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

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

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

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

Gin路由构建过程

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

ini 复制代码
 const (
   MethodGet     = "GET"
   MethodHead    = "HEAD"
   MethodPost    = "POST"
   MethodPut     = "PUT"
   MethodPatch   = "PATCH"
   MethodDelete  = "DELETE"
   MethodConnect = "CONNECT"
   MethodOptions = "OPTIONS"
   MethodTrace   = "TRACE"
 )

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

go 复制代码
 engine := &Engine{
     //省略其他代码
     trees: make(methodTrees, 0, 9),
     //省略其他代码
 }
 ​
 type methodTree struct {
   method string
   root   *node
 }
 ​
 // 路由树数组,每个HTTP Method对应一个元素
 type methodTrees []methodTree

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

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

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

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

c 复制代码
 const (
   static nodeType = iota
   root
   param
   catchAll
 )

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

sql 复制代码
 /order
 /user/list
 /user/add

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

bash 复制代码
 /user/:id

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

bash 复制代码
 /user/*id

图解Gin路由构建过程

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

bash 复制代码
 /user
 /user/list
 /user/delete
 /address/list
 /address/delete
 /admin/:id
 /admin/delete
 /admin/list/*name

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

go 复制代码
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
  //省略其他代码
  
	//获取对应请求方法的根节点
  root := engine.trees.get(method)
  //根节点为空时,创建根节点
	if root == nil {
		root = new(node)
		root.fullPath = "/"
		engine.trees = append(engine.trees, methodTree{method: method, root: root})
	}
  //添加路由
	root.addRoute(path, handlers)
  
  //省略其他代码
}

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

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

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

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

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

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

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

小结

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

相关推荐
訾博ZiBo几秒前
VibeCoding 时代来临:如何打造让 AI 秒懂、秒改、秒验证的“AI 友好型”技术栈?
前端·后端
Victor3562 小时前
Redis(25)Redis的RDB持久化的优点和缺点是什么?
后端
Victor3562 小时前
Redis(24)如何配置Redis的持久化?
后端
ningqw9 小时前
SpringBoot 常用跨域处理方案
java·后端·springboot
你的人类朋友9 小时前
vi编辑器命令常用操作整理(持续更新)
后端
胡gh9 小时前
简单又复杂,难道只能说一个有箭头一个没箭头?这种问题该怎么回答?
javascript·后端·面试
一只叫煤球的猫10 小时前
看到同事设计的表结构我人麻了!聊聊怎么更好去设计数据库表
后端·mysql·面试
uzong10 小时前
技术人如何对客做好沟通(上篇)
后端
颜如玉11 小时前
Redis scan高位进位加法机制浅析
redis·后端·开源
Moment11 小时前
毕业一年了,分享一下我的四个开源项目!😊😊😊
前端·后端·开源