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请求进行相应的处理。

相关推荐
颜淡慕潇25 分钟前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生1 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料1 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng3 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
paopaokaka_luck7 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风9 小时前
详解K8S--声明式API
后端
Peter_chq9 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
Yaml49 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍