Gin 框架底层原理(上) | 青训营
2023/8/23 ·雨辰login
前两篇发了 Golang HTTP 标准库的实现原理. 在此基础上做个延伸,这次聊聊 web 框架 Gin 的底层实现原理.
本篇内容引用自知乎用户@小徐先生.
这真的是一个宝藏博主,B站也有号:小徐先生1212
所以把他的笔记找来跟大家分享,希望大家都去看看,真的做的非常好。
1 Gin 与 HTTP
1.1 Gin 的背景
Gin 是 Golang 世界里最流行的 web 框架,于 github 开源:github.com/gin-gonic/g…
其中本文涉及到的源码走读部分,代码均取自 gin tag:v1.9.0 版本.
支撑研发团队选择 Gin 作为 web 框架的原因包括:
- 支持中间件操作( handlersChain 机制 )
- 更方便的使用( gin.Context )
- 更强大的路由解析能力( radix tree 路由树 )
括号中的知识点将在后文逐一介绍.
1.2 Gin 与 net/http 的关系
上周刚和大家一起探讨了 Golang net/http 标准库的实现原理,正是为本篇内容的学习打下铺垫. 没看过的同学建议先回到上篇内容中熟悉一下.
Gin 是在 Golang HTTP 标准库 net/http 基础之上的再封装,两者的交互边界如下图:
可以看出,在 net/http 的既定框架下,gin 所做的是提供了一个 gin.Engine 对象作为 Handler 注入其中,从而实现路由注册/匹配、请求处理链路的优化.
1.3 Gin 框架使用示例
下面提供一段接入 Gin 的示例代码,让大家预先感受一下 Gin 框架的使用风格:
- 构造 gin.Engine 实例:gin.Default()
- 路由组注册中间件:Engine.Use()
- 路由组注册 POST 方法下的 handler:Engine.POST()
- 启动 http server:Engine.Run()
import "github.com/gin-gonic/gin"
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 注册中间件
mux.Use(myMiddleWare)
// 注册一个 path 为 /ping 的处理函数
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// 运行 http 服务
if err := mux.Run(":8080"); err != nil {
panic(err)
}
}
2 注册 handler 流程
2.1 核心数据结构
首先看下核心数据结构:
(1)gin.Engine
type Engine struct {
// 路由组
RouterGroup
// ...
// context 对象池
pool sync.Pool
// 方法路由树
trees methodTrees
// ...
}
// net/http 包下的 Handler interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
}
Engine 为 Gin 中构建的 HTTP Handler,其实现了 net/http 包下 Handler interface 的抽象方法: Handler.ServeHTTP,因此可以作为 Handler 注入到 net/http 的 Server 当中.
Engine包含的核心内容包括:
- 路由组 RouterGroup:第(2)部分展开
- Context 对象池 pool:基于 sync.Pool 实现,作为复用 gin.Context 实例的缓冲池. gin.Context 的内容于本文第 5 章详解
- 路由树数组 trees:共有 9 棵路由树,对应于 9 种 http 方法. 路由树基于压缩前缀树实现,于本文第 4 章详解.
9 种 http 方法展示如下:
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
(2)RouterGroup
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
RouterGroup 是路由组的概念,其中的配置将被从属于该路由组的所有路由复用:
- Handlers:路由组共同的 handler 处理函数链. 组下的节点将拼接 RouterGroup 的公用 handlers 和自己的 handlers,组成最终使用的 handlers 链
- basePath:路由组的基础路径. 组下的节点将拼接 RouterGroup 的 basePath 和自己的 path,组成最终使用的 absolutePath
- engine:指向路由组从属的 Engine
- root:标识路由组是否位于 Engine 的根节点. 当用户基于 RouterGroup.Group 方法创建子路由组后,该标识为 false
(3)HandlersChain
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
HandlersChain 是由多个路由处理函数 HandlerFunc 构成的处理函数链. 在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc.
2.2 流程入口
下面以创建 gin.Engine 、注册 middleware 和注册 handler 作为主线,进行源码走读和原理解析:
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 注册中间件
mux.Use(myMiddleWare)
// 注册一个 path 为 /ping 的处理函数
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// ...
}
2.3 初始化 Engine
方法调用:gin.Default -> gin.New
- 创建一个 gin.Engine 实例
- 创建 Enging 的首个 RouterGroup,对应的处理函数链 Handlers 为 nil,基础路径 basePath 为 "/",root 标识为 true
- 构造了 9 棵方法路由树,对应于 9 种 http 方法
- 创建了 gin.Context 的对象池
路由树相关的内容见本文第 4 章;gin.Context 有关内容见本文第 5 章.
func Default() *Engine {
engine := New()
// ...
return engine
}
func New() *Engine {
// ...
// 创建 gin Engine 实例
engine := &Engine{
// 路由组实例
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
// ...
// 9 棵路由压缩前缀树,对应 9 种 http 方法
trees: make(methodTrees, 0, 9),
// ...
}
engine.RouterGroup.engine = engine
// gin.Context 对象池
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
2.3 注册 middleware
通过 Engine.Use 方法可以实现中间件的注册,会将注册的 middlewares 添加到 RouterGroup.Handlers 中. 后续 RouterGroup 下新注册的 handler 都会在前缀中拼上这部分 group 公共的 handlers.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
// ...
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
2.4 注册 handler
以 http post 为例,注册 handler 方法调用顺序为 RouterGroup.POST-> RouterGroup.handle,接下来会完成三个步骤:
- 拼接出待注册方法的完整路径 absolutePath
- 拼接出代注册方法的完整处理函数链 handlers
- 以 absolutePath 和 handlers 组成 kv 对添加到路由树中
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
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
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
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 数组并返回
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 章中详细拆解.
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)
// ...
}
原作者指路:小徐先生 - 知乎 (zhihu.com)