深入探究 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.轻松的可扩展性和高性能
在文档中探索更多!