Gin 框架源码阅读笔记

  最近在学习 Go 语言,简单学完语法以后,想找个框架看看。正好 Gin 框架的代码非常精炼,不算 test 和注释的话,只有四千多行,名字也比较好听,注释也写的比较完整,非常适合阅读,就果断 fork 一份开始看了。本篇记录下学习中的要点。

介绍

Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to httprouter. If you need performance and good productivity, you will love Gin.

  Gin 的官网对自己的介绍是 “高性能” 和 “良好生产效率” 的 web 框架。还提到了两个其它的库,第一个是 martini,它也是一个 web 框架,名字也是酒名,Gin 说自己用了它的 API,但是速度比它快 40 倍。使用了 httprouter 就是能够实现比 martini 快 40 倍的一个重要原因。
  在官网的 Features 中,有 8 个被列出来的关键功能,后面会慢慢看到这些功能的实现。

  • Fast
  • Middleware support
  • Crash-free
  • JSON validation
  • Routes grouping
  • Error management
  • Rendering built-in/Extendable

从一个小例子入手

  来看一个官网文档给出的最小例子。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080
}

  运行该例子,然后使用浏览器访问 http://localhost:8080/ping 即可得到一个 “pong”,233。
  这个例子很简单,拆分一下只有三步。

  1. 使用 gin.Default() 创建一个默认配置的 Engine 对象。
  2. 为 Engine 的 GET 方法的 “/ping” 地址注册一个回调函数,该函数会返回一个 “pong”。
  3. 启动 Engine,开始监听端口提供服务。

HTTP Method

  通过上面小例子中的 GET 方法可以看出在 Gin 中需要使用对应的同名函数注册 HTTP Method 的处理方法。
  HTTP Method 有九种方法,最常用的有四种,分别是 GET/POST/PUT/DELETE,分别对应了查询、插入、更新、删除四种功能。需要注意的一点是,Gin 还提供了 Any 接口,可以直接对一个地址绑定所有的 HTTP Method 处理方法。
  返回的结果一般包含两到三部分,其中 code 和 message 是一定会有的,data 一般用来表示额外的数据,如果没有额外数据需要返回的可以不使用。例子中的 200 就是 code 字段的值,pong 就是 message 字段的值。

创建 Engine 变量

  在上面的例子中,创建 Engine 使用的是 gin.Default(),不过这个函数是对 New 的封装,实际创建 Engine 都是通过 New 接口创建的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func New() *Engine {
    debugPrintWARNINGNew()
    engine := &Engine{
        RouterGroup: RouterGroup{
            // ... 初始化 RouterGroup 的字段
        },
        // ... 初始化其余字段
    }
    engine.RouterGroup.engine = engine // 将 engine 的指针保存在 RouterGroup 中
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}

  先来简单看一下创建的过程即可,暂时不去关注 Engine 结构中的各种成员变量的含义。可以看到 New 除了创建并初始化了一个 Engine 类型的变量 engine 以外,还把 engine.pool.New 设为了一个调用 engine.allocateContext() 的匿名函数,这个函数的作用后面再说。

注册路由回调函数

  在 Engine 中有一个内嵌结构体 RouterGroup,Engine 的与 HTTP Method 有关的接口都是继承自 RouterGroup 的,官网提到的功能点里的 Routes grouping 就是通过 RouterGroup 结构体来实现的。

1
2
3
4
5
6
type RouterGroup struct {
    Handlers HandlersChain // Group 自身的处理函数
    basePath string        // 关联的基本路径
    engine   *Engine       // 保存关联的 engine 对象
    root     bool          // root 标志,只有 Engine 默认创建的才是 true
}

  每个 RouterGroup 关联了一个基本路径 basePath,Engine 内嵌的 RouterGroup 的 basePath 为 “/” 。
  还有一组处理函数 Handlers,所有本组关联的路径下的请求都会额外执行本组的处理函数,主要是用于中间件的调用。Handlers 在 Engine 创建的时候是 nil,可以通过 Use 方法导入一组函数进去,后面会看到这个用法。

1
2
3
4
5
6
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 的 handle 方法是所有注册 HTTP Method 回调函数的最终入口,一开始那个例子中调用的 GET 以及其它与 HTTP Method 有关的方法都只是对 handle 方法的封装。
  handle 方法会根据 RouterGroup 的 basePath 和相对地址参数计算出绝对地址,同时调用 combineHandlers 方法得到最终的 handlers 数组。将这些结果作为参数,传给 Engine 的 addRoute 方法来注册处理函数。

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
}

  combineHandlers 方法做的事情就是创建了一个切片 mergedHandlers,然后先将 RouterGroup 本身的 Handlers 复制进去,再将参数的 handlers 复制进去,最后将 mergedHandlers 返回。也就是说使用 handle 注册任何方法的时候,实际的结果都是包括了 RouterGroup 本身的 Handlers 的。

使用基数树加速路由检索

  在官网提到的功能点的 Fast 里提到了网络请求的路由是基于基数树(Radix Tree)实现的。这部分并不是 Gin 实现的,而是开篇在 Gin 的介绍里提到的 httprouter 实现的,Gin 使用了 httprouter 来做了这部分的功能。有关于基数树的实现这里暂且不提,暂时只关注它的用法,可能后面会另开一篇专门写一下基数树的实现。
  在 Engine 中有一个 trees 变量,它是一个 methodTree 结构的切片,正是这个变量保存了所有的基数树的引用。

1
2
3
4
type methodTree struct {
    method string // method 的名字
    root   *node  // 链表的 root 节点指针
}

  Engine 为每个 HTTP Method 维护了一颗基数树,这棵树的 root 节点与 Method 的名字一起被保存在了一个 methodTree 变量中,所有的 methodTree 变量又都在 trees 里。

 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)
    // ... 省略一些代码
}

  可以看到在 Engine 的 addRoute 方法中,会首先使用 trees 的 get 方法拿到 method 对应的基数树的 root 节点。如果没有取到基数树的 root 节点,则说明之前没有对该 method 注册过方法,会创建一个树节点作为树的根节点并将其加入到 trees 中。
  在拿到 root 节点以后,使用 root 节点的 addRoute 方法为路径 path 注册一组处理函数 handlers,这一步就是为 path 和 handlers 创建一个节点存入基数树中,如果试图注册一个已经注册过的地址,addRoute 会直接抛出一个 panic 错误。
  在处理 HTTP 请求的时候,就需要通过 path 查找对应的节点的值。root 节点有一个 getValue 的方法负责处理查询的操作,后面讲到 Gin 处理 HTTP 请求的时候会提到。

导入中间件处理函数

  RouterGroup 的 Use 方法可以导入一组中间件处理函数。官网提到的功能点里的 Middleware support 就是通过 Use 方法实现的。
  在一开始的例子中,创建 Engine 结构体变量的时候并没有使用 New,而是使用了 Default,我们来看一下 Default 做了什么额外的东西。

1
2
3
4
5
6
func Default() *Engine {
    debugPrintWARNINGDefault()       // 输出日志
    engine := New()                  // 创建对象
    engine.Use(Logger(), Recovery()) // 导入中间件处理函数
    return engine
}

  可以看到它是个很简单的函数,除了调用 New 创建 Engine 对象以外,只对它调用了 Use 来导入了两个中间件函数,Logger 和 Recovery 的返回值。Logger 的返回值是用来记录日志的函数,Recovery 的返回值是用来处理 panic 的函数,这里我们先带过,后面再来看这两个函数。
  Engine 虽然内嵌了 RouterGroup,但是它本身也实现了 Use 方法,不过只是对 RouterGroup 的 Use 方法的调用,以及一些辅助操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}

func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

  可以看到 RouterGroup 的 Use 方法也很简单,就是将参数的中间件处理函数通过 append 加入到了自己的 Handlers 中。

开始运行

  在小例子中,最后一步是调用了 Engine 的 Run 方法,无参数。调用以后整个框架开始运行,使用浏览器访问注册好的地址就可以正确触发回调了。

1
2
3
4
5
6
7
func (engine *Engine) Run(addr ...string) (err error) {
    // ... 省略一些代码
    address := resolveAddress(addr) // 解析地址,默认地址是 0.0.0.0:8080
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}

  Run 只做了两件事情,解析地址和开启服务。这里的地址其实只需要传一个字符串,但是为了实现可传可不传,所以用的参数是一个变参。在 resolveAddress 中处理了 addr 的不同情况下的结果。
  开启服务使用的是标准库 net/http package 中的 ListenAndServe 方法,这个方法接受一个监听地址和一个 Handler 接口的变量。Handler 接口的定义很简单,只有一个 ServeHTTP 方法。

1
2
3
4
5
6
7
8
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

  因为 Engine 就实现了 ServeHTTP,这里会将 Engine 本身传给 ListenAndServe 方法,当监听的端口有新的连接时,ListenAndServe 会负责 accept 建立连接,并且在连接上有数据时,会调用 handler 的 ServeHTTP 方法进行处理。

处理消息

  Engine 的 ServeHTTP 是处理处理消息的回调函数,下面就来看一下它的内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    c := engine.pool.Get().(*Context) 
    c.writermem.reset(w)
    c.Request = req
    c.reset()

    engine.handleHTTPRequest(c) 

    engine.pool.Put(c) 
}

  回调函数有两个参数,第一个是用于接收请求回复的 w,将回复的数据写入到 w 即可,另一个是持有本次请求的数据的 req,可以从 req 里读后续处理所需的所有数据。
  ServeHTTP 做了四件事情,首先从 pool 池里拿取一个 Context,然后将 Context 与回调函数的参数绑定,接着使用 Context 作为参数调用 handleHTTPRequest 方法处理这个网络请求,最后将 Context 放回到池中。
  这里我们先只看 handleHTTPRequest 方法的核心部分。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
        // Find route in tree
        value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
        // ... 省略一些代码
        if value.handlers != nil {
            c.handlers = value.handlers
            c.fullPath = value.fullPath
            c.Next()
            c.writermem.WriteHeaderNow()
            return
        }
        // ... 省略一些代码
    }
    // ... 省略一些代码
}

  在 handleHTTPRequest 方法中最主要做的是两件事,首先根据请求的地址从基数树中拿取之前注册过的方法,这里会将 handlers 赋值给本次处理的 Context,然后调用 Context 的 Next 函数来执行 handlers 中的方法,最后在 Context 的 responseWriter 类型对象中写入本次请求的返回数据。

Context

  在处理 HTTP 请求的时候,所有的上下文相关数据都在 Context 变量中,作者也在 Context struct 的注释里写了 “Context is the most important part of gin”,足见它的重要性。
  在上面讲到 Engine 的 ServeHTTP 方法时,可以看到 Context 并不是直接创建的,而是通过 Engine 的 pool 变量的 Get 方法取得的,取出来以后先将其状态重置然后再使用,在使用完毕以后又被放回了 pool 中。
  Engine 的 pool 变量是一个 sync.Pool 类型的,此处只要知道它是 Go 官方提供的一个支持并发使用的对象池即可,可以通过它的 Get 方法从池中取出一个对象,也可以使用 Put 方法向池中投入一个对象。在池是空的时候如果使用 Get 方法,则它会通过自己的 New 方法来创建一个对象并将其返回。
  这个 New 方法正是在 Engine 的 New 方法中定义的,再来看一眼 Engine 的 New 方法。

1
2
3
4
5
6
7
func New() *Engine {
    // ... 省略其它代码
    engine.pool.New = func() any {
        return engine.allocateContext()
    }
    return engine
}

  从代码中可以看到 Context 的创建方法是 Engine 的 allocateContext 方法。在 allocateContext 方法中并没有什么玄机,只是做了两步切片长度的预分配,然后创建对象并返回。

1
2
3
4
5
func (engine *Engine) allocateContext() *Context {
    v := make(Params, 0, engine.maxParams)
    skippedNodes := make([]skippedNode, 0, engine.maxSections)
    return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}

  上文提到的 Context 的 Next 方法会执行 handlers 中的所有方法,来看一下它的实现。

1
2
3
4
5
6
7
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

  虽然 handlers 是一个切片,但是 Next 方法并未简单实现成一个对 handlers 的遍历,而是引入了一个处理进度的记录 index,它被初始化为 0,在方法一开始的时候执行累加,在一个方法执行结束以后再次执行累加。
  Next 被设计成这样跟它的用法有很大关系,主要是为了跟一些中间件函数配合。比如当某个 handler 执行的时候触发了 panic,在中间件中可以使用 recover 接住错误,然后再次调用 Next,即可继续执行后面的 handler,不会因为一个 handler 的问题影响到整个 handlers 数组。

处理 panic

  在 Gin 中,如果某个请求的处理函数触发了 panic,整个框架并不会直接 crash,而是抛出一条错误信息,然后继续提供服务。有点像 Lua 框架一般会使用 xpcall 来执行消息的处理函数一样。这个操作就是官方文档提到的功能点 Crash-free 了。
  上文提到,当使用 gin.Default 创建一个 Engine 的时候,会执行 Engine 的 Use 方法导入两个函数,其中的一个是 Recovery 函数的返回值,它又是对其它函数的封装,最后调用到的函数是 CustomRecoveryWithWriter,来看一下这个函数的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
    // ... 省略其它代码
    return func(c *Context) {
        defer func() {
            if err := recover(); err != nil {
                // ... 错误处理代码
            }
        }()
        c.Next() // 执行下一个 handler
    }
}

  这里不关注错误处理的细节,只看它做的事情。这个函数返回了一个匿名函数,在匿名函数中使用 defer 注册了另一个匿名函数,在这个匿名函数中使用了 recover 接住了 panic,然后进行错误处理。在处理结束以后,调用了 Context 的 Next 方法,使原先正在依次执行的 Context 的 handlers 可以继续执行下去。

Built with Hugo
主题 StackJimmy 设计