公众号:程序员读书,欢迎关注
在这篇文章中,我们来深入探究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
是一个路由树根节点的指针,其类型为node
,node
节点是用于存储路由树节点的数据结构,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
的子节点,而list
和delete
作为/
的子节点:
添加/address/list
节点时,该节点与根节点/user
的共同前缀为/
,因此将/
提取出来作为根节点,创建address/list
节点,把user
和address/list
节点作为根节点/
的子节点:
添加/address/delete
节点时,同样是从根节点一层层查找,与根节点/
的共同前缀为/
,继续往下查找,与address/list
有共同前缀address/
,将address/list
分裂为address/
,list
和delete
节点,address/
节点作为根节点的子节点,list
和delete
作为address/
的子节点:
添加/admin/:id
节点,仍然是从根节点查找共同前缀,或分裂或创建节点,根据前面添加节点的经验,我们觉得结果应该是这样的:
但是由于该节点包含有通配符:
,因此会进一步分裂通配符节点,因此最终的结果为:
包含通配符:
节点虽然是第一个被添加的节点,但会放在最后一个节点,比如我们添加/admin/delete
后,结果如下:
添加/admin/list/*name
节点时,对于包含通配符*
的节点,Gin
框架会在该节点前创建一个空白节点。
从代码层面理解Gin路由构建
接下来,我们再从代码层面来了解Gin
是如何实现上述示意图所演示的路由构建逻辑的。
我们知道在Gin
框架中,可以调用gin.Engine
实例的GET
,POST
等方法添加路由,比如:
css
engine := gin.New()
engine.GET("/user/list")
而实际上添加路由的方法其底层最终都是调用了gin.Engine
的addRoute
方法,该方法与添加路由相关代码如下:
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
的根节点root
,root
的类型为node
。 - 如果没有找到路由根节点,则以
/
为根路径创建一个根节点,并加入到engine.trees
中。 - 调用根节点
root(类型为node)
的addRoute
方法从树中寻找合适的位置创建路由节点。
接下来我们来看看node
的addRoute
方法,Gin
框架添加路由的最主要逻辑就在这个方法中:
node
的addRoute
的代码很长,其主要执行流程如下图所示:
node
的insertChild
方法也是添加路由主要方法,其代码如下:
insertChild
方法主要方法首先会判断所要添加的路径是否包含通配符,主要分三种情况:
- 如果不包含通配符,则直接将路径添加到当前节点。
- 包含通配符
:
时,创建子节点,如果后面有子路径,继续循环执行。 - 包含通配符
*
时,创建一个空节点,创建一个通配符节点,将通配符节点添加到空节点后面。
小结
在Gin
框架底层采用Radix Tree
作为路由树的存储结构,由于其能够快速处理大量具有共同前缀的数据,使得Gin
能够为Web
应用程序提供出色的性能和用户体验。通过使用Radix Tree
,Gin
能够快速、准确地找到匹配的路由,从而对HTTP
请求进行相应的处理。