目录
1.Gin 的背景
Gin 是 Golang 世界里最流行的 web 框架,于 github 开源:https://github.com/gin-gonic/gin
其中本文涉及到的源码走读部分,代码均取自 gin tag:v1.9.0 版本。
主要优点:
-
支持中间件操作( handlersChain 机制 )
-
更方便的使用( gin.Context )
-
更强大的路由解析能力( radix tree 路由树 )
2.Gin 与 net/http 的关系
Gin 是在 Golang HTTP 标准库 net/http 基础之上的再封装
3.Gin 框架使用示例
下面提供一段接入 Gin 的示例代码:
-
构造 gin.Engine 实例:gin.Default()
-
路由组注册中间件:Engine.Use()
-
路由组注册 POST 方法下的 handler:Engine.POST()
-
启动 http server:Engine.Run()
go get -u github.com/gin-gonic/gin
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
// 创建一个默认的 gin 路由器
router := gin.Default()
// 注册中间件
//router.Use(MiddleWare)
// 设置一个路由处理器
router.GET("/", func(c *gin.Context) {
c.String(200, "Hello, World!")
})
// 设置 POST 请求处理器
router.POST("/post", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "This is a POST request",
})
})
// 启动 HTTP 服务器
router.Run(":8080") // 监听在 8080 端口
}
4.注册 handler 流程
4.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
-
Context 对象池 pool:基于 sync.Pool 实现,作为复用 gin.Context 实例的缓冲池. gin.Context 的内容于本文第 5 章详解
-
路由树数组 trees:共有 9 棵路由树,对应于 9 种 http 方法. 路由树基于压缩前缀树实现。
9 种 http 方法展示如下:
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
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
HandlersChain
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
HandlersChain 是由多个路由处理函数 HandlerFunc 构成的处理函数链. 在使用的时候,会按照索引的先后顺序依次调用 HandlerFunc.
4.2.流程
下面以创建 gin.Engine 、注册 middleware 和注册 handler 作为主线,进行源码走读和原理解析
func main() {
// 创建一个默认的 gin 路由器
router := gin.Default()
// 注册中间件
router.Use(MiddleWare)
// 设置 POST 请求处理器
router.POST("/post", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "This is a POST request",
})
})
// ...
}
4.3.初始化 Engine
方法调用:gin.Default -> gin.New
-
创建一个 gin.Engine 实例
-
创建 Enging 的首个 RouterGroup,对应的处理函数链 Handlers 为 nil,基础路径 basePath 为 "/",root 标识为 true
-
构造了 9 棵方法路由树,对应于 9 种 http 方法
-
创建了 gin.Context 的对象池
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
}
4.4.注册 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()
}
4.5.注册 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()
}
完整路径拼接
结合 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
}
完整 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
}
注册 handler 到路由树
-
获取 http method 对应的 methodTree
-
将 absolutePath 和对应的 handlers 注册到 methodTree 中
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)
// ...
}
4.6.中间件中的next原理
在 gin
框架中,next
函数实际上是一个闭包,它代表了中间件链中的下一个中间件或最终的路由处理器。next
函数是在 gin
框架内部生成的,并通过 gin.HandlerFunc
类型传递给中间件函数。下面我将详细介绍 next
函数的实现原理。
gin
中间件的工作原理
-
定义中间件:
- 中间件函数是一个接受
gin.HandlerFunc
参数的函数。 - 这个函数返回一个
gin.HandlerFunc
,它包含了中间件的逻辑。
- 中间件函数是一个接受
-
注册中间件:
- 使用
router.Use
方法来注册中间件。 - 这些中间件按照注册顺序排列,形成一个中间件栈。
- 使用
-
调用中间件:
- 当请求到达时,
gin
框架会从中间件栈的第一个中间件开始执行。 - 每个中间件都会调用
next(c)
来执行下一个中间件或路由处理器。 next(c)
是一个闭包,它保存了中间件栈的信息,并负责调用下一个中间件。
- 当请求到达时,
next
函数的实现
在 gin
框架内部,next
函数是通过闭包实现的。下面是一个简化的示例,展示 next
函数是如何在 gin
框架中实现的:
// 定义一个中间件
func loggingMiddleware(next gin.HandlerFunc) gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("Request received at:", c.Request.URL.Path)
// 调用下一个中间件或处理函数
next(c)
fmt.Println("Response sent for:", c.Request.URL.Path)
}
}
gin
内部实现
在 gin
框架内部,中间件栈和 next
函数的实现更加复杂。下面是一个简化的示例,展示 gin
如何实现中间件栈和 next
函数:
// 假设这是简化版的 gin.HandlerFunc 类型
type ginFunc func(*gin.Context)
// 假设这是简化版的 gin.Context 类型
type ginContext struct{}
// 假设这是简化版的 gin.RouterGroup 类型
type ginRouterGroup struct {
middlewares []ginFunc
}
// Use 方法注册中间件
func (rg *ginRouterGroup) Use(middlewares ...ginFunc) {
rg.middlewares = append(rg.middlewares, middlewares...)
}
// Handle 方法设置路由处理器
func (rg *ginRouterGroup) Handle(method string, path string, handlers ...ginFunc) {
// 假设这里有代码来处理路由和处理器的绑定
}
// Next 是一个闭包,它保存了中间件栈的信息
func (c *ginContext) Next() {
// 这里假设有一个方法来执行下一个中间件
}
// 一个示例中间件
func loggingMiddleware(next ginFunc) ginFunc {
return func(c *ginContext) {
fmt.Println("Before next()")
next(c) // 调用下一个中间件或处理函数
fmt.Println("After next()")
}
}
func main() {
router := &ginRouterGroup{}
// 注册中间件
router.Use(loggingMiddleware)
// 设置一个路由处理器
router.Handle("GET", "/", func(c *ginContext) {
c.Next()
})
// 启动 HTTP 服务器
// 这里假设有一个启动服务器的方法
}
在 gin
框架中,next
函数是一个闭包,它负责调用下一个中间件或处理函数。这个闭包是 gin
框架内部实现的一部分,用于维护中间件栈的执行流程。当你在中间件函数中调用 next(c)
时,实际上是调用了 gin
框架提供的闭包,这个闭包知道如何调用下一个中间件或处理函数。
5.启动服务流程
5.1.流程入口
下面通过 Gin 框架运行 http 服务为主线,进行源码走读:
func main() {
// 创建一个 gin Engine,本质上是一个 http Handler
mux := gin.Default()
// 一键启动 http 服务
if err := mux.Run(); err != nil{
panic(err)
}
}
5.2 启动服务
一键启动 Engine.Run 方法后,底层会将 gin.Engine 本身作为 net/http 包下 Handler interface 的实现类,并调用 http.ListenAndServe 方法启动服务.
func (engine *Engine) Run(addr ...string) (err error) {
// ...
err = http.ListenAndServe(address, engine.Handler())
return
}
顺便多提一嘴,ListenerAndServe 方法本身会基于主动轮询 + IO 多路复用的方式运行,因此程序在正常运行时,会始终阻塞于 Engine.Run 方法,不会返回.
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)
}
}
5.3 处理请求
在服务端接收到 http 请求时,会通过 Handler.ServeHTTP 方法进行处理. 而此处的 Handler 正是 gin.Engine,其处理请求的核心步骤如下:
-
对于每笔 http 请求,会为其分配一个 gin.Context,在 handlers 链路中持续向下传递
-
调用 Engine.handleHTTPRequest 方法,从路由树中获取 handlers 链,然后遍历调用
-
处理完 http 请求后,会将 gin.Context 进行回收. 整个回收复用的流程基于对象池管理
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)
}
Engine.handleHTTPRequest 方法核心步骤分为三步:
-
根据 http method 取得对应的 methodTree
-
根据 path 从 methodTree 中找到对应的 handlers 链
-
将 handlers 链注入到 gin.Context 中,通过 Context.Next 方法按照顺序遍历调用 handler
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++ {
// 获取对应的方法树
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 从路由树中寻找路由
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
// ...
}