Gin 框架底层原理(上) | 青训营

474 阅读5分钟

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)