一、Gin框架的路由原理:
参考:
go路由httprouter中的压缩字典树算法图解及c++实现 Golang-gin框架路由原理
首先了解下什么是路由?
简而言之,http路由即是一条http请求的“向导”,根据URI上的路径,指引该条请求到对应的方法里去执行然后返回,中间可能会执行一些中间件。其次,路由又分为 静态路由,动态路由…
静态路由: 框架/用户提前生成一个路由表,一般是map结构,key为URL上的path,value为代码执行点(处理函数),
优点:只需要读取map,没有任何开销,速度快缺点:无法正则匹配路由,只能逐一对应,模糊匹配的场景无法使用 动态路由: 用户定义好路由匹配规则,框架匹配路由时,根据规则动态的去规划路由
优点:适应性强,解决了静态路由的缺点缺点:相比静态路由有开销,具体视算法和路由匹配规则而定
gin框架作为一个轻量级的web框架,采用的是字典树(前缀树)的方式实现的动态路由,不支持路由的正则匹配。当 gin 注册路由时,会根据不同的 Method 分别注册不同的路由树,比如 这四个请求,会分别注册四棵路由树出来:
GET /user/{userID} HTTP/1.1
POST /user/{userID} HTTP/1.1
PUT /user/{userID} HTTP/1.1
DELETE /user/{userID} HTTP/1.1
gin路由注册的示例代码及其前缀树示意图,分别如下:
r := gin.New()
r.GET("/user/:name", routeUser)
func routeUser(c *gin.Context){
//todo something
}
关于更多字典树的细节可以看这里:
① 字典树(Trie树):是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
优点:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。复杂度分析:字典树其实是一种用空间换时间的算法,它占用的空间一般很大,但非常高效,插入和查询的时间复杂度都是 O(1) 的。
然而普通的字典树每个节点只能存储一个字符,这意味着面对较长的字符串仍然要向下探寻多个节点,这存在着浪费,因此就有了压缩字典树。
② 压缩字典树:是trie树的一种,也称单词查找树(前缀树),善于进行字符串检索、取字符串最长公共前缀、以及排序,常应用在搜索引擎中,例如:百度输入某个字可能自动弹出匹配到的词组出来。 ③ 字典树与压缩字典树的区别:
压缩字典树 每个节点不仅仅只存一个字符,而是取字符串的最长公共前缀。压缩字典树和标准字典树最大的不同点就节点的数量与插入字符串的个数成正比,而不是与字符串的长度成正比,所以当字符串数量越来越多,越密集且相似度极高的情况下,会退化成标准trie树。
下面分别是 /,/bear,/bell,/bid,/bull,/buy,/sell,/stock,/stop的 标准tire 和 压缩 tire 的示意图:
④ 关于字典树的查询操作 过程如下图:
先找共同前缀,比如下图中的s再找目录 indices,比如下图中的eu循环上面两步,直至当前path满足条件
二、Gin框架的中间件原理(Middleware)
参考:
go面试题 - gin中间件原理分析bilibili视频 - gin的中间件原理和简单手撸
中间件是为了过滤路由而发明的一种机制,也就是http请求来到时先经过中间件,再到具体的处理函数。
首先,Gin框架的中间件是基于洋葱模型的,如下图: beforeFunc1和afterFunc1即是中间件1;afterFunc2和afterFunc2即是中间件2。
过程:请求到来时从最外层开始执行中间件1,然后进入第二层,依次执行完所有中间件最后到达主体函数,接着再一层一层的往外走再次执行中间件2…中间件1…最后返回,也有点像栈的概念。
其次,再来看下gin.Context的结构体的主要字段:
// gin.Context 结构体
type Context struct {
...
handlers HandlersChain // 函数指针切片对象
index int8 // 对应函数指针切片中的索引下标,执行c.Next()时会向后移动index下标位置
...
}
type HandlerFunc func(*Context) // 函数指针
type HandlersChain []HandlerFunc // 函数指针切片
可以看到,gin框架的中间件函数和处理函数是以切片形式的调用链条存在的(本质上就是函数指针切片) 在我们初始化了gin对象之后:r := gin.New()
注册 [中间件函数/路由处理函数] 的过程: r.Use(),也就是不断的在上述的 HandlersChain 函数指针切片后执行append操作,去依次向调用链条追加新注册的中间件函数
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
调用 [中间件函数/路由处理函数] 的过程: 如下图,gin框架正是通过移动切片下标 index 的位置,实现中间件的不断向后调用… 但是这个index要如何去移动呢?这就是gin框架中 c.Next()的作用了,这个函数会在每次调用时将index向后移动,从而依次调用已注册的中间件。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
停止调用后续 [中间件函数/路由处理函数] 的过程: c.Abort()方法会阻止调用后续的中间件处理函数,正是因为它使得index移动到切片末尾了,所以后面的 [中间件/路由处理函数] 都没法继续执行了。
func (c *Context) Abort() {
c.index = abortIndex
}
因此,gin框架的 [中间件函数/路由处理函数] 实际上都是以切片的形式的调用链条存在的(本质上就是函数指针切片),我们可以顺序调用也可以借助 c.Next() 方法实现嵌套调用。
三、总结
gin框架路由使用字典树(前缀树),路由注册的过程是构造前缀树的过程,路由匹配的过程就是查找前缀树的过程。另外,gin框架不支持路由的正则匹配。gin框架的 [中间件函数和处理函数] 是以切片的形式的调用链条存在的(本质上就是函数指针切片),我们可以顺序调用也可以借助 c.Next() 方法实现嵌套调用。借助c.Set()和c.Get()方法我们能够在不同的中间件函数中传递数据。