参考资料
本文核心思路参考了 小徐先生 的博客文章,在此表示感谢:
- 作者:小徐先生
- 原文标题:解析 Gin 框架底层原理
1
Gin
与HTTP
1.1 Gin
与 net/http
的关系
Gin
是在 Golang HTTP
标准库 net/http
基础之上的再封装,具体体现在:
- 路由层:用压缩前缀树替代原生的
ServeMux
(原生仅支持简单前缀匹配)。 - 上下文层:通过
gin.Context
封装请求 / 响应操作,替代原生的http.ResponseWriter
和*http.Request
。 - 中间件层:通过
HandlersChain
实现洋葱模型,而原生需手动链式调用。 两者的交互边界如下图:图中以背景颜色的不同可以分为三个模块。
- 以蓝色为底的这部分是
Gin
框架增强的内容。 - 以红色为底的这部分也是从属于
Gin
框架的内容,为了支持Gin
框架使用的数据结构。 - 以黄色为底的这部分则是golang标准库
net/http
当中的核心内容。
Gin
框架使用示例
下面提供一段接入 Gin
的示例代码,让大家预先感受一下 Gin
框架的使用风格:
- 构造
gin.Engine
实例:gin.Default()
- 路由组注册中间件:
Engine.Use()
- 路由组注册
POST
方法下的handler
:Engine.POST()
- 启动
http server
:Engine.Run()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
// 中间件的作用是可以前置的把一些公共的逻辑抽离出来放在中间件中去实现
// 可以理解成TCP服务端去处理请求时使用到的公共的切面
// 在服务端运行时每次收到请求时都会先经过中间件,然后才会执行对应的handler
var myMiddleWare = func (ctx *gin.Context) {
fmt.Println("my middleware is running...")
}
func main () {
engine := gin.Default() // 构造出gin.Engine实例
engine.Use(myMiddleWare) // middleware给注入到engine中作为中间件来使用
engine.POST("/ping", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("pong"))
})
// 运行 http 服务
if err := engine.Run(":8080"); err != nil {
panic(err)
}
}
当我们把服务端运行起来时再用一个终端去扮演客户端的角色,输入curl -X POST 127.0.0.1:8080/ping
我们就能看到客户端对应的输出了一个pong
,是不是老神奇了!再去看服务端就能看到我们在代码中写的中间件的输出。
2 注册 handler
流程
2.1 核心数据结构
sync.Pool
:用来帮助我们去复用对应的一系列的gin.Context
。我们前面说了每一笔HTTP
请求到来之后我们都会为这笔请求去分配gin.Context
来供这笔请求去使用。那作为一个服务端来说他接受到的请求可能是有很高的并发量,gin.Context
在这过程中可能会被不断的创造和销毁。所以我们不妨采用一个回收站的思想,我们每一次去创造gin.Context
时不会去立刻去内存构造出一个新的实例,而是先看一眼在这个回收站当中是不是还残存着对应的之前被回收的实例,如果有的话我们优先复用(类似于缓存)。同时这个回收站还有一个自动清理垃圾的能力,当两轮GC之后对应gin.Context
里面一系列的实例都还没有被使用到它会自动清理垃圾把这部分的内存做一个回收。(具体内容会在第五章当中详细探讨)RouterGroup
路由组:指的是后续我们针对每一笔具体的请求都会有一个路径处理函数,这个路由组当中的所有配置会被这个组当中所有成员也就是这一系列的请求路径对应的这个HandlersChain
所共同使用,包括basePath
,就是每一个RouterGroup
可以有一个公共的前缀路径,后续在这个Group
下面的具体的每一个子路径会去基于这个basePath
作为前缀,拼接上自身的相对路径来生成一个对应的绝对路径。RouterGroup
当中可以完成一个当前这个组所公共的中间件的注入,比如说我们定义了一个用户模块,这个模块可能会有一些公共的脱落业务逻辑之外的一些逻辑需要放在同一个切面当中执行,比如去校验用户是否登录了,如果登录的话需要把更多的用户信息获取到注入到gin.Context
中供后续的请求链路使用。trees methodTrees
路由树:根据http
各种请求方法一共分成九棵树,每一棵树会根据我们的请求路径基于压缩前缀树的一套规则来实现一个父子节点的拆分和合并,最终把对应于每一个路径的HandlerChain
所挂载在这个路径所对应的节点下面。后续当有对应请求到来之后先根据这个请求对应的HTTPMethod
映射到具体那棵树,再根据他的请求路径去树当中寻找到对应到的节点再去获取到他对应的HandlersChain
做逻辑的处理。
下面我们来详细讲述一下上面说的三个部分
(1)gin.Engine
Engine
包含的核心内容包括:
- 路由组
RouterGroup
:第(2)部分展开 Context
对象池pool
:基于sync.Pool
实现,作为复用gin.Context
实例的缓冲池.gin.Context
的内容于本文第 5 章详解- 路由树数组
trees
:共有 9 棵路由树,对应于 9 种http
方法. 路由树基于压缩前缀树实现,于本文第 4 章详解.
1
2
3
4
5
6
7
8
9
10
type Engine struct {
// 路由组
RouterGroup
// ...
// context 对象池
pool sync.Pool
// 方法路由树
trees methodTrees
// ...
}
Engine
为 Gin
中构建的 HTTP Handler
,其实现了 net/http
包下 Handler interface
的抽象方法: Handler.ServeHTTP
,因此可以作为 Handler
注入到 net/http
的 Server
当中。所有实现了这个ServeHTTP
方法的类型我们都可以把它认为是handler
的一个实现类,都可以把它注入到net/http
到运行框架中用于处理后续到来的 http
请求。
1
2
3
4
5
6
7
8
9
// net/http 包下的 Handler interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// 实现了ServeHTTP方法因此他就是http包下的handler的具体实现类
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
}
每一棵 engine
都有九棵路由树,9 种 http 方法展示如下:
1
2
3
4
5
6
7
8
9
10
11
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
(2)RouterGroup
RouterGroup
是路由组的概念,其中的配置将被从属于该路由组的所有路由复用:
1
2
3
4
5
6
7
8
9
10
11
12
type RouterGroup struct {
// Handlers:路由组共同的 handler 处理函数链. 组下的节点将拼接 RouterGroup 的公用 handlers 和自己的 handlers,组成最终使用的 handlers 链
Handlers HandlersChain
// basePath:路由组的基础路径. 组下的节点将拼接 RouterGroup 的 basePath 和自己的 path,组成最终使用的 absolutePath
basePath string
// engine:指向路由组从属的 Engine
engine *Engine
// root:标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false
root bool
}
(3)HandlersChain
HandlersChain
是由多个路由处理函数 HandlerFunc
构成的处理函数链。在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc
.
1
2
3
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context) // 对应的逻辑处理函数
2.2 流程入口
我们用第一章使用的案例代码来作为我们讲解的流程入口,大概分成三条线。
1
2
3
4
5
6
7
8
9
10
11
12
func main () {
// 1
engine := gin.Default() // 构造出 gin.Engine 实例
// 2
engine.Use(myMiddleWare) // middleware 给注入到 engine 中作为中间件来使用
// 3
engine.POST("/ping", func(ctx *gin.Context) {
ctx.Writer.Write([]byte("pong"))
})
}
- 首先第一步是去构造出一个
gin.Engine
实例; - 调用
Engine.Use
方法去实现了我们自己定制的中间件的注入; - 完成了一个具体的路径以及对应的处理函数的注册。 下面就来详细的讲述这三个步骤。
1. 初始化 Engine
方法调用:gin.Default
-> gin.New
- 创建一个
gin.Engine
实例 - 创建
Engine
的首个RouterGroup
,对应的处理函数链Handlers
为nil
,基础路径basePath
为 “/”,root
标识为true
- 构造了 9 棵方法路由树,对应于 9 种
http
方法 - 创建了
gin.Context
的对象池 路由树相关的内容见本文第 4 章;gin.Context 有关内容见本文第 5 章.
1
2
3
4
5
func Default() *Engine {
engine := New()
// ...
return engine
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func New() *Engine {
// ...
// 1. 创建 gin Engine 实例,绑定一个路由组
engine := &Engine{
// 2. RouterGroup路由组实例
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/", // 基础路径
root:
},
// ...
// 3. 9 棵路由压缩前缀树,对应 9 种 http 方法
trees: make(methodTrees, 0, 9),
// ...
}
engine.RouterGroup.engine = engine
// 4. gin.Context 对象池
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
到这里为止Engine实例初始化就完成了。✅
2. 注册middleware
通过 Engine.Use
方法可以实现中间件的注册,会将注册的 middlewares
添加到 RouterGroup.Handlers
中. 后续 RouterGroup
下新注册的 handler
都会在前缀中拼上这部分 group
公共的 handlers
.
1
2
3
4
5
6
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
// 中间件会被注入到当前所属的RouterGroup当中
engine.RouterGroup.Use(middleware...)
// ...
return engine
}
1
2
3
4
5
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
// 把我们注入的一系列中间件的处理函数给追加到RouterGroup所对应的HandlersChain的切片当中
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
3. 注册handler
)
以
http post
为例,注册 handler
方法调用顺序为 RouterGroup.POST
-> RouterGroup.handle
,接下来会完成三个步骤:
- 拼接出待注册方法的完整路径
absolutePath
:首先看当前这个路径是从属于哪一个具体的路由组,获取到这个路由组当中的basePath
作为路由组的公共前缀,拼接上自身的一个相对路径最终能够得到一个绝对路径absolutePath
。 - 拼接出代注册方法的完整处理函数链
handlers
:获取到对应路由组当中所共有的中间件,再拼接上我们此时注册操作所注入的这部分新的HandlersChain
,最终形成一个完整的HandlersChain
。 - 以
absolutePath
和handlers
组成 kv 对添加到路由树中:比如说我们当前用的是POST
方法,因此会走到POST
方法所对应的那棵路由的压缩前缀树当中,然后根据absolutePath
去做一个压缩前缀树的一个检索的过程,最终找到对应的节点,如果有的话我们就复用这个节点,如果没有的话我们就新建出一个对应的节点然后把刚刚我们拼接出来的完整的HandlersChain
挂载到对应的节点上。
1
2
3
4
// 以POST请求为例
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
1
2
3
4
5
6
7
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
// 会分为我们上述详细描述的三个步骤
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
(1)完整路径拼接
结合 RouterGroup
中的 basePath
和注册时传入的 relativePath
,组成 absolutePath
1
2
3
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
1
2
3
4
5
6
7
8
9
10
11
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
(2)完整 handlers
生成
深拷贝 RouterGroup
中 handlers
和注册传入的 handlers
,生成新的 handlers
数组并返回
1
2
3
4
5
6
7
8
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
(3)注册 handler
到路由树
- 获取
http method
对应的methodTree
- 将
absolutePath
和对应的handlers
注册到methodTree
中 路由注册方法root.addRoute
的信息量比较大,放在本文第 4 章中详细拆解.
1
2
3
4
5
6
7
8
9
10
11
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) // 第四章见分晓
// ...
}
3 启动服务流程
3.1 流程入口
下面通过 Gin
框架运行 http
服务为主线,进行源码走读:
1
2
3
4
5
6
7
8
9
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 一键启动 http 服务
if err := mux.Run(); err != nil{ // 进入阻塞态
panic(err)
}
}
3.2 启动服务
一键启动 Engine.Run
方法后,底层会将 gin.Engine
本身作为 net/http
包下 Handler interface
的实现类,并调用 http.ListenAndServe
方法启动服务。
1
2
3
4
5
func (engine *Engine) Run(addr ...string) (err error) {
// ...
err = http.ListenAndServe(address, engine.Handler()) // 这个大家都熟悉了吧
return
}
顺便多提一嘴,ListenerAndServe
方法本身会基于主动轮询 + IO 多路复用的方式运行,因此程序在正常运行时,会始终阻塞于 Engine.Run
方法,不会返回.
1
2
3
4
5
6
7
8
9
10
11
12
13
func (srv *Server) Serve(l net.Listener) error {
// ...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
// ...
connCtx := ctx
// ...
c := srv.newConn(rw)
// ...
go c.serve(connCtx)
}
}
这部分小徐先生也讲过,详细可以去看看他的博客Golang HTTP 标准库实现原理
3.3 处理请求
在服务端接收到 http
请求时,会通过 Handler.ServeHTTP
方法进行处理. 而此处的 Handler
正是 gin.Engine
,其处理请求的核心步骤如下:
- 对于每笔
http
请求,会为其分配一个gin.Context
,在handlers
链路中持续向下传递 - 调用
Engine.handleHTTPRequest
方法,从路由树中获取handlers
链,然后遍历调用 - 处理完
http
请求后,会将gin.Context
进行回收。整个回收复用的流程基于对象池管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取一个 context,如果对象池中有的话直接复用,如果没有的话再去创建新的 context
c := engine.pool.Get().(*Context)
// 重置/初始化 context
c.writermem.reset(w)
c.Request = req // 把对应 HTTP 请求 request 注入到 context 中
c.reset() // 如果这个 context 是之前已经使用过然后放回对象池中去回收复用的,这时我们要把它前置放入的一些业务数据给清除掉,让他变成一个崭新的 context 捏
// 处理 http 请求,真正处理请求的逻辑函数,下面详细讲讲
engine.handleHTTPRequest(c)
// 把 context 放回对象池,给后续的请求去复用
engine.pool.Put(c)
}
Engine.handleHTTPRequest
方法核心步骤分为三步:
- 根据
http method
取得对应的methodTree
- 根据
path
从methodTree
中找到对应的handlers
链 - 将
handlers
链注入到gin.Context
中,通过Context.Next
方法按照顺序遍历调用handler
此处根据 path
从路由树寻找 handlers
的逻辑位于 root.getValue
方法中,和路由树数据结构有关,放在本文第 4 章详解;
根据 gin.Context.Next 方法遍历 handler 链的内容放在本文第 5 章详解.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// ...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
// 1. 遍历对应的 9 棵方法树去找到对应的方法树
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 2. 从路由树中寻找路由
// 获取压缩前缀树的根节点,根据路径做检索匹配,获取到对应的 HandlersChain
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
// 如果获取成功了我们这个 HandlersChain 给挂载在当前这个 Context 实例当中
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
// 3. 去遍历执行对应的 HandlersChain ,完成对应的逻辑处理
c.Next()
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
// ...
}
4 Gin的路由树
4.1 策略与原理
在聊 Gin
路由树实现原理之前,需要先补充一个压缩前缀树 radix tree
的基础设定.
(1)前缀树
前缀树又称 trie
树,是一种基于字符串公共前缀构建索引的树状结构,核心点包括:
- 除根节点之外,每个节点对应一个字符
- 从根节点到某一节点,路径上经过的字符串联起来,即为该节点对应的字符串
- 尽可能复用公共前缀,如无必要不分配新的节点
画一个图就很好理解啦
(2)压缩前缀树
压缩前缀树又称基数树或 radix
树,是对前缀树的改良版本,优化点主要在于空间的节省,核心策略体现在:
- 倘若某个子节点是其父节点的唯一孩子,则与父节点进行合并
在
gin
框架中,使用的正是压缩前缀树的数据结构。比如在这棵树中我们要去找BCE字符串,E是C唯一的孩子,C又是B唯一的孩子,那我们就可以把这三个节点放在同一个节点当中,让原本表示B的节点表示BCE
如果说我们要查找ABC这个单词,那我们能不能把C和D节点合并在一起呢? 当然不行!如果合并的话就找不到以C结尾的单词了,ABC这个单词就失效了。
(3)为什么使用压缩前缀树
与压缩前缀树相对的就是使用 hashmap
,以 path
为 key
,handlers
为 value
进行映射关联,这里选择了前者的原因在于:
path
匹配时不是完全精确匹配,比如末尾/
符号的增减、全匹配符号*
的处理等,map
无法胜任。举个例子比如说/user/ping
和/user/get
应该被统一映射到/user/*
这样的路径中,这时就是一个模糊匹配的过程,map 无法胜任,压缩前缀树能更好支持前缀模糊匹配的能力。- 路由的数量相对有限,对应数量级下
map
的性能优势体现不明显,在小数据量的前提下,map
性能甚至要弱于前缀树 path
串通常存在基于分组分类的公共前缀,适合使用前缀树进行管理,可以节省存储空间
(4)补偿策略
在 Gin
路由树中还使用一种补偿策略,在组装路由树时,会将注册路由句柄数量更多的 child node
摆放在 children
数组更靠前的位置.
这是因为某个链路注册的 handlers
句柄数量越多,一次匹配操作所需要花费的时间就越长,且被匹配命中的概率就越大,因此应该被优先处理。
那我去检索一个请求路径时都会遵循从左往右以此遍历它的子节点看看是不是能够命中匹配下一段路径,因此这个子节点的位置越靠左,就可以越优先被遍历到。
4.2 核心数据结构
下面聊一下路由树的数据结构,对应于 9 种 http method,共有 9 棵 methodTree. 每棵 methodTree 会通过 root 指向 radix tree 的根节点.
1
2
3
4
type methodTree struct {
method string
root *node // 根节点
}
node
是 radix tree
中的节点,对应节点含义如下:
path
:节点的相对路径,拼接上RouterGroup
中的basePath
作为前缀后才能拿到完整的路由path
indices
:由各个子节点path
首字母组成的字符串,子节点顺序会按照途径的路由数量priority
进行排序priority
:途径本节点的路由数量,反映出本节点在父节点中被检索的优先级children
:子节点列表handlers
:当前节点对应的处理函数链
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type node struct {
// 节点的相对路径
path string
// 每个 indice 字符对应一个孩子节点的 path 首字母
indices string
// ...
// 后继节点数量
priority uint32
// 孩子节点列表
children []*node
// 处理函数链
handlers HandlersChain
// path 拼接上前缀后的完整路径
fullPath string
}
4.3 注册到路由树
承接本文 2.4 小节第(3)部分,下述代码展示了将一组 path + handlers 添加到 radix tree 的详细过程,核心位置均已给出注释,此处就不再赘述了,请大家尽情享用源码盛宴吧!(这部分原作者真的讲得很好,可以去看看原视频gin注册路由流程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// 插入新路由
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
// 每有一个新路由经过此节点,priority 都要加 1
n.priority++
// 加入当前节点为 root 且未注册过子节点,则直接插入并返回,该节点就成为根节点
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
// 外层 for 循环断点
walk:
for {
// 获取 node.path 和待插入路由 path 的最长公共前缀长度
i := longestCommonPrefix(path, n.path)
// 倘若最长公共前缀长度小于 node.path 的长度,代表 node 需要分裂
// 举例而言:node.path = search,此时要插入的 path 为 see
// 最长公共前缀长度就是 2,len(n.path) = 6
// 需要分裂为 se -> arch
// -> e
if i < len(n.path) {
// 原节点分裂后的后半部分,对应于上述例子的 arch 部分
child := node{
path: n.path[i:],
// 原本 search 对应的参数都要托付给子节点 arch
indices: n.indices,
children: n.children,
handlers: n.handlers,
// 新路由 see 进入时,先将 search 的 priority 加 1 了,此时需要扣除 1 并赋给 arch
priority: n.priority - 1,
fullPath: n.fullPath,
}
// 先建立 search -> arch 的数据结构,后续调整 search 为 se
n.children = []*node{&child}
// 设置 se 的 indice 首字母为 a
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
// 调整 search 为 se
n.path = path[:i]
// search 的 handlers 都托付给 arch 了,se 本身没有 handlers
n.handlers = nil
// ...
}
// 最长公共前缀长度小于 path,正如 se 之于 see
if i < len(path) {
// path see 扣除公共前缀 se,剩余 e
path = path[i:]
c := path[0]
// 根据 node.indices,辅助判断,其子节点中是否与当前 path 还存在公共前缀
for i, max := 0, len(n.indices); i < max; i++ {
// 倘若 node 子节点还与 path 有公共前缀,则令 node = child,并调到外层 for 循环 walk 位置开始新一轮处理
if c == n.indices[i] {
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// node 已经不存在和 path 再有公共前缀的子节点了,则需要将 path 包装成一个新 child node 进行插入
// node 的 indices 新增 path 的首字母
n.indices += bytesconv.BytesToString([]byte{c})
// 把新路由包装成一个 child node,对应的 path 和 handlers 会在 node.insertChild 中赋值
child := &node{
fullPath: fullPath,
}
// 新 child node append 到 node.children 数组中
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
// 令 node 指向新插入的 child,并在 node.insertChild 方法中进行 path 和 handlers 的赋值操作
n = child
n.insertChild(path, fullPath, handlers)
return
}
// 此处的分支是,path 恰好是其与 node.path 的公共前缀,则直接复制 handlers 即可
// 例如 se 之于 search
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
// ...
return
}
1
2
3
4
5
6
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
// ...
n.path = path
n.handlers = handlers
// ...
}
呼应于 4.1 小节第(4)部分谈到的补偿策略,下面这段代码体现了,在每个 node 的 children 数组中,child node 在会依据 priority 有序排列,保证 priority 更高的 child node 会排在数组前列,被优先匹配.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
cs[pos].priority++
prio := cs[pos].priority
// Adjust position (move to front)
newPos := pos
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// Build new index char string
if newPos != pos {
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
}
4.4 检索路由树
承接本文 3.3 小节,下述代码展示了从路由树中匹配 path
对应 handler
的详细过程,请大家结合注释消化源码吧.
1
2
3
4
5
type nodeValue struct {
// 处理函数链
handlers HandlersChain
// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 从路由树中获取 path 对应的 handlers
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16
// 外层 for 循环断点
walk:
for {
prefix := n.path // 当前节点相对路径
// 待匹配 path 长度大于 node.path
if len(path) > len(prefix) {
// node.path 长度 < path,且前缀匹配上
if path[:len(prefix)] == prefix {
// path 取为后半部分
path = path[len(prefix):]
// 遍历当前 node.indices,找到可能和 path 后半部分可能匹配到的 child node
idxc := path[0]
for i, c := range []byte(n.indices) {
// 找到了首字母匹配的 child node
if c == idxc {
// 将 n 指向 child node,调到 walk 断点开始下一轮处理
n = n.children[i]
continue walk
}
}
// ...
}
}
// 倘若 path 正好等于 node.path,说明已经找到目标
if path == prefix {
// ...
// 取出对应的 handlers 进行返回
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
// ...
}
// 倘若 path 与 node.path 已经没有公共前缀,说明匹配失败,会尝试重定向,此处不展开
// ...
}
5 Gin.Context
5.1 核心数据结构
gin.Context 的定位是对应于一次 http 请求,贯穿于整条 handlersChain 调用链路的上下文,其中包含了如下核心字段:
Request/Writer
:http
请求和响应的reader
、writer
入口,用于建立和客户端之间的通信,Request可以理解为是用来读取客户端传输数据的入口,而writer则是我们当前这个服务端处理完对应的请求之后把响应的数据发送回客户端的出口。handlers
:本次http
请求对应的处理函数链index
:当前的处理进度,即处理链路处于函数链的索引位置engine
:Engine
的指针mu
:用于保护map
的读写互斥锁Keys
:缓存handlers
链上共享数据的map
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Context struct {
// ...
// http 请求参数
Request *http.Request
// http 响应 writer
Writer ResponseWriter
// ...
// 处理函数链
handlers HandlersChain
// 当前处于处理函数链的索引
index int8
engine *Engine
// ...
// 读写锁,保证并发安全
mu sync.RWMutex
// key value 对存储 map
Keys map[string]any
// ..
}
5.2 复用策略
gin.Context
作为处理 http
请求的通用数据结构,不可避免地会被频繁创建和销毁. 为了缓解 GC 压力,gin
中采用对象池 sync.Pool
进行 Context
的缓存复用,处理流程如下:
http
请求到达时,从pool
中获取Context
,倘若池子已空,通过pool.New
方法构造新的Context
补上空缺http
请求处理完成后,将Context
放回pool
中,用以后续复用sync.Pool
并不是真正意义上的缓存,将其称为回收站或许更加合适,放入其中的数据在逻辑意义上都是已经被删除的,但在物理意义上数据是仍然存在的,这些数据可以存活两轮 GC 的时间,在此期间倘若有被获取的需求,则可以被重新复用.
1
2
3
4
type Engine struct {
// context 对象池
pool sync.Pool
}
1
2
3
4
5
6
7
func New() *Engine {
// ...
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
1
2
3
4
5
func (engine *Engine) allocateContext(maxParams uint16) *Context {
v := make(Params, 0, maxParams)
// ...
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
5.3 分配与回收时机
gin.Context
分配与回收的时机是在 gin.Engine
处理 http
请求的前后,位于 Engine.ServeHTTP
方法当中:
- 从池中获取
Context
- 重置
Context
的内容,使其成为一个空白的上下文 - 调用
Engine.handleHTTPRequest
方法处理http
请求 - 请求处理完成后,将
Context
放回池中
1
2
3
4
5
6
7
8
9
10
11
12
13
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取一个 context
c := engine.pool.Get().(*Context)
// 重置/初始化 context
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理 http 请求
engine.handleHTTPRequest(c)
// 把 context 放回对象池
engine.pool.Put(c)
}
5.4 使用时机
(1)handlesChain
入口
在 Engine.handleHTTPRequest
方法处理请求时,会通过 path
从 methodTree
中获取到对应的 handlers
链,然后将 handlers
注入到 Context.handlers
中,然后启动 Context.Next
方法开启 handlers
链的遍历调用流程.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (engine *Engine) handleHTTPRequest(c *Context) {
// ...
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
// ...
if value.handlers != nil {
c.handlers = value.handlers // 挂载到context当中
c.fullPath = value.fullPath
c.Next() // 执行 HandlersChain 中的方法
c.writermem.WriteHeaderNow()
return
}
// ...
}
// ...
}
(2)handlesChain
遍历调用
推进
handlers
链调用进度的方法正是 Context.Next
. 可以看到其中以 Context.index
为索引,通过 for 循环依次调用 handlers
链中的 handler
.
1
2
3
4
5
6
7
8
func (c *Context) Next() {
// index表示当前遍历到了处理函数链的哪一个位置
c.index++ // 这里++是因为context初始化之后index初始值是-1,保证index是从0开始的
for c.index < int8(len(c.handlers)) { // 遍历的终点就是handlers函数链的长度
c.handlers[c.index](c) // 执行处理函数
c.index++
}
}
由于 Context
本身会暴露于调用链路中,因此用户可以在某个 handler
中通过手动调用 Context.Next
的方式来打断当前 handler
的执行流程,提前进入下一个 handler
的处理中.
由于此时本质上是一个方法压栈调用的行为,因此在后置位 handlers
链全部处理完成后,最终会回到压栈前的位置,执行当前 handler
剩余部分的代码逻辑.
结合下面的代码示例来说,用户可以在某个
handler
中,于调用 Context.Next
方法的前后分别声明前处理逻辑和后处理逻辑,这里的“前”和“后”相对的是后置位的所有 handler
而言.
1
2
3
4
5
6
7
func myHandleFunc(c *gin.Context){
// 前处理
preHandle()
c.Next()
// 后处理
postHandle()
}
此外,用户可以在某个 handler
中通过调用 Context.Abort
方法实现 handlers
链路的提前熔断.
其实现原理是将 Context.index
设置为一个过载值 63,导致 Next
流程直接终止. 这是因为 handlers
链的长度必须小于 63,否则在注册时就会直接 panic
. 因此在 Context.Next
方法中,一旦 index
被设为 63,则必然大于整条 handlers
链的长度,for 循环便会提前终止.
1
2
3
4
5
const abortIndex int8 = 63
func (c *Context) Abort() {
c.index = abortIndex // 把index值设置成一个非法值来强制终止流程
}
此外,用户还可以通过 Context.IsAbort
方法检测当前 handlerChain
是出于正常调用,还是已经被熔断.
1
2
3
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
注册 handlers
,倘若 handlers
链长度达到 63,则会 panic
1
2
3
4
5
6
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
// 断言 handlers 链长度必须小于 63
assert1(finalSize < int(abortIndex), "too many handlers")
// ...
}
(3)共享数据存取
gin.Context
作为 handlers
链的上下文,还提供对外暴露的 Get
和 Set
接口向用户提供了共享数据的存取服务,相关操作都在读写锁的保护之下,能够保证并发安全.
1
2
3
4
5
6
7
8
type Context struct {
// ...
// 读写锁,保证并发安全
mu sync.RWMutex
// key value 对存储 map
Keys map[string]any
}
1
2
3
4
5
6
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
return
}
1
2
3
4
5
6
7
8
9
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
}
c.Keys[key] = value
}
6 总结
gin
将Engine
作为http.Handler
的实现类进行注入,从而融入 Golangnet/http
标准库的框架之内gin
中基于handler
链的方式实现中间件和处理函数的协调使用gin
中基于压缩前缀树的方式作为路由树的数据结构,对应于 9 种http
方法共有 9 棵树gin
中基于gin.Context
作为一次http
请求贯穿整条handler chain
的核心数据结构gin.Context
是一种会被频繁创建销毁的资源对象,因此使用对象池sync.Pool
进行缓存复用
鸣谢与参考
本文在整理过程中参考了以下资料 特此致谢: 小徐先生 - 解析 Gin 框架底层原理