深入探究 Gin:Golang 的领先框架

Image description

介绍

Image description

Gin 是一个用 Go (Golang) 编写的 HTTP Web 框架。它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您需要超强性能,那就选择 Gin 吧。

Gin 的官方网站将自己介绍为一个“高性能”和“生产力好”的 Web 框架。它还提到了另外两个库。第一个是 Martini,也是一个 Web 框架,名字叫酒。Gin 说它使用了它的 API,但速度快了 40 倍。使用 `httprouter` 是它能比 Martini 快 40 倍的重要原因。

在官网的“Features”中,列出了八个重点功能,我们将在后续逐步看到这些功能的实现。

  • 快速地
  • 中间件支持
  • 无碰撞
  • JSON 验证
  • 路线分组
  • 错误管理
  • 内置/可扩展渲染
  • 从一个小例子开始

    我们先来看官方文档给出的最小的例子。

    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”的结果。

    这个例子很简单,可以分成三步:

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

    从上面小例子中的`GET`方法我们可以看出,在Gin中,HTTP方法的处理方法需要使用对应同名函数进行注册。

    HTTP 方法共有 9 种,最常用的四种是 `GET`、`POST`、`PUT`、`DELETE`,分别对应查询、插入、更新、删除四种功能。需要注意的是,Gin 还提供了 `Any` 接口,可以直接将所有 HTTP 方法处理方式绑定到一个地址上。

    返回结果一般包含两三部分,其中`code`和`message`是固定的,`data`一般用来表示附加数据,如果没有附加数据需要返回,可以省略,例子中200是`code`字段的值,"pong"是`message`字段的值。

    创建引擎变量

    上面的例子中,我们通过 `gin.Default()` 创建了 `Engine`,但这个函数其实是对 `New` 的一个包装,实际上 `Engine` 是通过 `New` 接口创建的。

    func New() *Engine {
        debugPrintWARNINGNew()
        engine := &Engine{
            RouterGroup: RouterGroup{
                //... Initialize the fields of RouterGroup
            },
            //... Initialize the remaining fields
        }
        engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
        engine.pool.New = func() any {
            return engine.allocateContext()
        }
        return engine
    }

    现在只简单看一下创建过程,先不要关注 `Engine` 结构体中各个成员变量的含义。可以看到,`New` 除了创建并初始化一个 `Engine` 类型的 `engine` 变量之外,还将 `engine.pool.New` 设置为一个调用 `engine.allocateContext()` 的匿名函数。这个函数的作用后面会讲到。

    注册路由回调函数

    `Engine` 中有一个内嵌的结构体 `RouterGroup`,`Engine` 中与 HTTP 方法相关的接口均继承自 `RouterGroup`,官网提到的功能点中的“路由分组”就是通过 `RouterGroup` 结构体实现的。

    type RouterGroup struct {
        Handlers    HandlersChain // Processing functions of the group itself
        basePath    string        // Associated base path
        engine      *Engine       // Save the associated engine object
        root        bool          // root flag, only the one created by default in Engine is true
    }

    每一个 `RouterGroup` 都关联一个基础路径 `basePath`,内嵌在 `Engine` 中的 `RouterGroup` 的 `basePath` 为 "/"。

    另外还有一组处理函数`Handlers`,所有关联到这一组的路径下的请求都会额外执行这一组的处理函数,主要用于中间件调用。`Handlers`在`Engine`创建时为`nil`,可以通过`Use`方法传入一组函数,后面我们会看到这种用法。

    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 方法回调函数的最终入口,初始示例中调用的 `GET` 方法和其他与 HTTP 方法相关的方法只是 `handle` 方法的包装器。

    `handle` 方法会根据 `RouterGroup` 的 `basePath` 和相对路径参数计算出绝对路径,同时调用 `combineHandlers` 方法得到最终的 `handlers` 数组。这些结果作为参数传递给 `Engine` 的 `addRoute` 方法,用于注册处理函数。

    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` 结构的一部分。这个变量保存了对所有基数树的引用。

    type methodTree struct {
        method string // Name of the method
        root   *node  // Pointer to the root node of the linked list
    }

    `Engine` 为每一个 HTTP 方法维护一个基数树,这棵树的根节点和方法名一起保存在一个 `methodTree` 变量中,所有的 `methodTree` 变量都在 `trees` 中。

    func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
        //... Omit some code
        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)
        //... Omit some code
    }

    可以看到,在`Engine`的`addRoute`方法中,会先使用`trees`的`get`方法获取`method`对应的radix树的根节点,如果没有获取到radix树的根节点,说明之前没有为这个`method`注册过方法,则会创建一个树节点作为树的根节点,并添加到`trees`中。

    获取到根节点之后,使用根节点的 `addRoute` 方法为路径 `path` 注册一组处理函数 `handlers`。这步就是为 `path` 和 `handlers` 创建一个节点,并存放到 radix tree 中。如果尝试注册一个已经注册过的地址,`addRoute` 会直接抛出 `panic` 错误。

    在处理 HTTP 请求时,需要通过 `path` 找到对应节点的值。根节点有一个 `getValue` 方法负责处理查询操作。在讲 Gin 处理 HTTP 请求时我们会提到这一点。

    导入中间件处理函数

    `RouterGroup` 的 `Use` 方法可以导入一组中间件处理函数,官网提到的功能点中的“中间件支持”就是通过 `Use` 方法实现的。

    在最初的例子中,创建 `Engine` 结构体变量时,并没有使用 `New`,而是使用了 `Default`。我们来看看 `Default` 额外做了什么。

    func Default() *Engine {
        debugPrintWARNINGDefault()       // Output log
        engine := New()                  // Create object
        engine.Use(Logger(), Recovery()) // Import middleware processing functions
        return engine
    }

    可以看出是一个非常简单的函数,除了调用 New 创建 Engine 对象之外,只调用 Use 传入 Logger 和 Recovery 两个中间件函数的返回值。Logger 的返回值是用于记录日志的函数,Recovery 的返回值是用于处理 panic 的函数。这里先略过,以后再看这两个函数。

    `Engine` 虽然内嵌了 `RouterGroup`,也实现了 `Use` 方法,但只是调用了 `RouterGroup` 的 `Use` 方法以及一些辅助操作而已。

    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` 方法,调用之后整个框架就开始运行了,用浏览器访问注册的地址就能正确触发回调。

    func (engine *Engine) Run(addr...string) (err error) {
        //... Omit some code
        address := resolveAddress(addr) // Parse the address, the default address is 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` 包中的 `ListenAndServe` 方法,该方法接受一个监听地址和一个 `Handler` 接口的变量。`Handler` 接口的定义很简单,只有一个 `ServeHTTP` 方法。

    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` 会负责接受并建立连接,当连接上有数据时,就会调用 `handler` 的 `ServeHTTP` 方法进行处理。

    处理消息

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

    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` 方法的核心部分。

    func (engine *Engine) handleHTTPRequest(c *Context) {
        //... Omit some code
        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)
            //... Omit some code
            if value.handlers!= nil {
                c.handlers = value.handlers
                c.fullPath = value.fullPath
                c.Next()
                c.writermem.WriteHeaderNow()
                return
            }
            //... Omit some code
        }
        //... Omit some code
    }

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

    语境

    在处理 HTTP 请求时,所有与上下文相关的数据都在 `Context` 变量中。作者在 `Context` struct 的注释中也写道“Context 是 gin 中最重要的部分”,可见其重要性。

    在上面讲到 `Engine` 的 `ServeHTTP` 方法时可以看出,`Context` 并不是直接创建的,而是通过 `Engine` 的 `pool` 变量的 `Get` 方法获取的,取出后在使用前重置其状态,使用完后再放回池中。

    `Engine` 的 `pool` 变量是 `sync.Pool` 类型,目前只需要知道它是 Go 官方提供的一个支持并发使用的对象池。可以通过它的 `Get` 方法从池中获取一个对象,也可以使用 `Put` 方法将一个对象放入池中。当池为空时,使用 `Get` 方法,它会通过自身的 `New` 方法创建一个对象并返回。

    这个`New`方法是在`Engine`的`New`方法中定义的,我们再来看一下`Engine`的`New`方法。

    func New() *Engine {
        //... Omit other code
        engine.pool.New = func() any {
            return engine.allocateContext()
        }
        return engine
    }

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

    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 里的所有方法,我们来看一下它的实现。

    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` 继续执行后续的 `handlers`,而不会因为某个 `handler` 的问题而影响整个 `handlers` 数组。

    处理恐慌

    在 Gin 中,如果某个请求的处理函数触发了 `panic`,整个框架并不会直接 ​​crash,而是抛出一个错误信息,服务仍然会继续提供。这有点类似于 Lua 框架通常使用 `xpcall` 来执行消息处理函数的方式。这个操作就是官方文档中提到的“Crash-free”特性点。

    上面提到,使用 gin.Default 创建 Engine 时,会执行 Engine 的 Use 方法,传入两个函数,其中一个是 Recovery 函数的返回值,是其他函数的包装器,最终调用的函数是 CustomRecoveryWithWriter,我们来看看这个函数的实现。

    func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
        //... Omit other code
        return func(c *Context) {
            defer func() {
                if err := recover(); err!= nil {
                    //... Error handling code
                }
            }()
            c.Next() // Execute the next handler
        }
    }

    这里我们不关注错误处理的细节,只看它做了什么。这个函数返回一个匿名函数,在这个匿名函数里面,使用 `defer` 注册了另一个匿名函数,在这个内部匿名函数里面,使用 `recover` 来捕获 `panic`,然后进行错误处理。处理完成后,会调用 `Context` 的 `Next` 方法,这样原本按顺序执行的 `Context` 的 `handlers` 就可以继续执行了。

    Leapcell:用于 Web 托管、异步任务和 Redis 的下一代无服务器平台

    最后给大家介绍一下部署Gin服务的最佳平台:Leapcell。

    Image description

    1. 多语言支持

  • 使用 JavaScript、Python、Go 或 Rust 进行开发。
  • 2. 免费部署无限项目

  • 仅按使用量付费 — 无请求,无费用。
  • 3.无与伦比的成本效率

  • 按需付费,无闲置费用。
  • 例如:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。
  • 4. 简化的开发人员体验

  • 直观的用户界面,轻松设置。
  • 完全自动化的 CI/CD 管道和 GitOps 集成。
  • 实时指标和日志记录可提供可操作的见解。
  • 5.轻松的可扩展性和高性能

  • 自动扩展以轻松处理高并发。
  • 零运营开销——只需专注于建设。
  • 在文档中探索更多!