Gin 是一个用 Go(Golang)编写的 HTTP Web 框架。它具有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您需要精彩的表演,那就给自己来点杜松子酒吧。
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,就会得到“乒乓”声。
这个例子非常简单。它可以分为三个步骤:
从上面小例子中的GET方法我们可以看出,在Gin中,HTTP方法的处理方法需要使用对应的同名函数进行注册。
HTTP 方法有九种,最常用的四种是 GET、POST、PUT 和 DELETE,分别对应查询、插入、更新和删除四种功能。需要注意的是,Gin还提供了Any接口,可以直接将所有HTTP方法处理方法绑定到一个地址。
返回的结果一般包含两部分或三部分。代码和消息始终存在,数据通常用于表示附加数据。如果没有额外的数据要返回,则可以省略。在示例中,200 是 code 字段的值,“pong”是 message 字段的值。
在上面的示例中,gin.Default() 用于创建引擎。然而,这个函数是 New 的包装。其实Engine就是通过New接口创建的。
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 }
暂时简单看一下创建过程,不要关注Engine结构体中各个成员变量的含义。可以看到,New除了创建并初始化一个Engine类型的引擎变量外,还将engine.pool.New设置为一个调用engine.allocateContext()的匿名函数。这个函数的作用后面会讲。
Engine 中有一个嵌入的 struct RouterGroup。 Engine的HTTP方法相关接口均继承自RouterGroup。官网提到的功能点中的“路由分组”是通过RouterGroup结构体实现的。
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 }
每个 RouterGroup 都与一个基本路径 basePath 相关联。 Engine 中嵌入的 RouterGroup 的基本路径是“/”。
还有一组处理函数Handlers。所有与该组关联的路径下的请求都会额外执行该组的处理函数,主要用于中间件调用。 Engine创建时Handlers为nil,可以通过Use方法导入一组函数。我们稍后会看到这个用法。
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的handle方法是注册所有HTTP方法回调函数的最终入口。初始示例中调用的 GET 方法和其他与 HTTP 方法相关的方法只是对 handle 方法的包装。
handle方法会根据RouterGroup的basePath和相对路径参数计算出绝对路径,同时调用combineHandlers方法得到最终的handlers数组。这些结果作为参数传递给Engine的addRoute方法来注册处理函数。
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() }
combineHandlers方法的作用是创建一个切片mergedHandlers,然后将RouterGroup本身的Handler复制到其中,然后将参数的handler复制到其中,最后返回mergedHandlers。也就是说,当使用handle注册任何方法时,实际结果包括RouterGroup本身的Handler。
在官网提到的“快速”特征点中,提到网络请求的路由是基于基数树(Radix Tree)实现的。这部分并不是由Gin实现的,而是由一开始介绍Gin时提到的httprouter实现的。 Gin使用httprouter来实现这部分功能。基数树的实现这里暂时不讲。我们现在只关注它的用法。也许稍后我们会单独写一篇文章来介绍基数树的实现。
在引擎中,有一个 trees 变量,它是 methodTree 结构的一个切片。正是这个变量保存了对所有基数树的引用。
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 方法维护一个基数树。这棵树的根节点和方法的名称一起保存在一个methodTree变量中,所有methodTree变量都在树中。
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的addRoute方法中,首先会使用trees的get方法来获取该方法对应的radix树的根节点。如果没有获取到基数树的根节点,则说明之前没有为该方法注册过任何方法,将会创建一个树节点作为树的根节点,并添加到树中。
获取根节点后,使用根节点的addRoute方法注册一组针对路径path的处理函数handler。这一步是为路径和处理程序创建一个节点并将其存储在基数树中。如果你尝试注册一个已经注册的地址,addRoute会直接抛出一个panic错误。
在处理HTTP请求时,需要通过路径找到对应节点的值。根节点有一个getValue方法负责处理查询操作。我们在谈论 Gin 处理 HTTP 请求时会提到这一点。
RouterGroup的Use方法可以导入一组中间件处理函数。官网提到的功能点中的“中间件支持”是通过Use方法实现的。
在最初的示例中,创建Engine结构体变量时,没有使用New,而是使用了Default。让我们看看 Default 额外做了什么。
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 }
可以看出,这是一个非常简单的函数。除了调用New创建Engine对象外,只调用Use导入Logger和Recovery两个中间件函数的返回值。 Logger的返回值是用于记录日志的函数,Recovery的返回值是用于处理panic的函数。我们暂时跳过这个,稍后再看这两个函数。
虽然Engine内嵌了RouterGroup,也实现了Use方法,但只是调用了RouterGroup的Use方法以及一些辅助操作。
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 }
可见RouterGroup的使用方法也非常简单。它只是通过append将参数的中间件处理功能添加到自己的Handler中。
在这个小例子中,最后一步是不带参数调用 Engine 的 Run 方法。调用后,整个框架开始运行,用浏览器访问注册地址即可正确触发回调。
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 }
Run方法只做两件事:解析地址和启动服务。这里地址其实只需要传一个字符串就可以了,但是为了达到能传能不能传的效果,使用了一个可变参数。 resolveAddress方法处理addr不同情况的结果。
启动服务使用标准库的net/http包中的ListenAndServe方法。该方法接受一个监听地址和一个Handler接口的变量。 Handler接口的定义非常简单,只有一个ServeHTTP方法。
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() }
因为Engine实现了ServeHTTP,所以Engine本身将被传递到这里的ListenAndServe方法。当监听端口有新的连接时,ListenAndServe会负责接受并建立连接,当连接上有数据时,会调用handler的ServeHTTP方法进行处理。
Engine的ServeHTTP是处理消息的回调函数。我们来看看它的内容。
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 }
回调函数有两个参数。第一个是w,用于接收请求回复。将回复数据写入w。另一个是req,保存本次请求的数据。后续处理所需的所有数据都可以从req中读取
ServeHTTP 方法做了四件事。首先从pool池中获取一个Context,然后将Context与回调函数的参数绑定,然后以Context为参数调用handleHTTPRequest方法来处理这次网络请求,最后将Context放回池中。
我们首先只看handleHTTPRequest方法的核心部分。
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 }
handleHTTPRequest方法主要做了两件事。首先根据请求的地址从基数树中获取之前注册的方法。这里,handlers会被分配到Context中进行本次处理,然后调用Context的Next函数来执行handlers中的方法。最后将本次请求的返回数据写入Context的responseWriter类型对象中。
处理 HTTP 请求时,所有与上下文相关的数据都在 Context 变量中。作者还在Context结构体的注释中写到“Context is the most important part of gin”,可见其重要性。
上面讲Engine的ServeHTTP方法时可以看出,Context并不是直接创建的,而是通过Engine的pool变量的Get方法获取的。取出后,使用前重置其状态,使用后放回池中。
Engine 的池变量的类型为sync.Pool。目前只知道它是Go官方提供的支持并发使用的对象池。您可以通过 Get 方法从池中获取对象,也可以使用 Put 方法将对象放入池中。当池为空并且使用Get方法时,它会通过自己的New方法创建一个对象并返回。
这个New方法是在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 }
从代码中可以看出Context的创建方法是Engine的allocateContext方法。 allocateContext 方法并没有什么神秘之处。它只是对切片长度进行两步预分配,然后创建对象并返回它。
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 }
上面提到的 Context 的 Next 方法将执行处理程序中的所有方法。我们来看看它的实现。
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() }
虽然handlers是一个切片,但是Next方法并不是简单地实现为handlers的遍历,而是引入了一个处理进度记录索引,该索引初始化为0,在方法开始时递增,在方法结束后再次递增执行完成。
Next的设计和它的用法有很大关系,主要是为了配合一些中间件功能。例如,当某个handler执行过程中触发panic时,可以使用中间件中的recover捕获错误,然后再次调用Next继续执行后续的handler,而不会因为该问题影响整个handlers数组一名处理程序。
在Gin中,如果某个请求的处理函数触发了panic,整个框架并不会直接崩溃。相反,将抛出错误消息,并且将继续提供服务。这有点类似于Lua框架通常使用xpcall来执行消息处理函数。这个操作就是官方文档中提到的“Crash-free”特性点。
上面提到,使用 gin.Default 创建 Engine 时,会执行 Engine 的 Use 方法来导入两个函数。其中之一是 Recovery 函数的返回值,它是其他函数的包装。最终调用的函数是CustomRecoveryWithWriter。我们来看看这个函数的实现。
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 }
这里我们不关注错误处理的细节,而只看看它做了什么。该函数返回一个匿名函数。在这个匿名函数中,使用defer注册了另一个匿名函数。在这个内部匿名函数中,使用recover来捕获panic,然后进行错误处理。处理完成后,调用Context的Next方法,这样Context原本按顺序执行的处理程序就可以继续执行。
最后给大家介绍一下部署Gin服务最好的平台:Leapcell。
在文档中探索更多内容!
Leapcell Twitter:https://x.com/LeapcellHQ
以上是深入探讨 Gin:Golang 的领先框架的详细内容。更多信息请关注PHP中文网其他相关文章!