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

介绍

Gin 是一个用 Go (Golang) 编写的 HTTP Web 框架。它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您需要超强性能,那就选择 Gin 吧。
Gin 的官方网站将自己介绍为一个“高性能”和“生产力好”的 Web 框架。它还提到了另外两个库。第一个是 Martini,也是一个 Web 框架,名字叫酒。Gin 说它使用了它的 API,但速度快了 40 倍。使用 `httprouter` 是它能比 Martini 快 40 倍的重要原因。
在官网的“Features”中,列出了八个重点功能,我们将在后续逐步看到这些功能的实现。
从一个小例子开始
我们先来看官方文档给出的最小的例子。
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”的结果。
这个例子很简单,可以分成三步:
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。

1. 多语言支持
2. 免费部署无限项目
3.无与伦比的成本效率
4. 简化的开发人员体验
5.轻松的可扩展性和高性能
在文档中探索更多!