深入探究 Gin:Golang 的领先框架
介绍
从一个小例子开始
HTTP 方法
创建引擎变量
注册路由回调函数
使用基数树加速路由检索
导入中间件处理函数
开始运行
处理消息
语境
处理恐慌
介绍
Gin是一个用 Go (Golang) 编写的 HTTP Web 框架。它拥有类似 Martini 的 API,但性能比 Martini 快 40 倍。如果您追求极致性能,Gin 是您的理想之选。
Gin 的官网将自己介绍为一个“高性能”和“良好生产力”的 Web 框架。它还提到了另外两个库。第一个是 Martini,它也是一个 Web 框架,名字像酒一样。Gin 表示它使用了 Martini 的 API,但速度却快了 40 倍。使用 Martinihttprouter是它能比 Martini 快 40 倍的重要原因。
官网的“功能”部分列出了八个关键功能,我们将在后续逐步看到这些功能的实现。
- 快速地
- 中间件支持
- 无碰撞
- 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具有默认配置的对象。 GET在方法中为“/ping”地址注册一个回调函数Engine。该函数将返回一个“pong”。- 启动
Engine即可开始监听端口并提供服务。
HTTP 方法
从GET上面小例子中的方法可以看出,在Gin中,HTTP方法的处理方法需要使用对应的同名函数进行注册。HTTP
方法共有九种,最常用的四种是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结构体中各个成员变量的含义。可以看到,除了创建并初始化一个engine类型的变量之外Engine,New还将 设置engine.pool.New为一个调用 的匿名函数engine.allocateContext()。该函数的作用稍后会讨论。
注册路由回调函数
RouterGroup中内嵌了一个struct Engine,其中与HTTP方法相关的接口Engine均继承自RouterGroup,官网提到的功能点中的“路由分组”就是通过该RouterGroupstruct实现的。
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。basePath嵌入RouterGroup在 中的Engine是“/”。
此外还有一组处理函数Handlers。所有关联到该组路径下的请求都会额外执行该组的处理函数,这些函数主要用于中间件调用。Handlers在创建nil时Engine,可以通过 方法来传入一组函数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()
}
handle的方法是RouterGroup注册所有 HTTP 方法回调函数的最终入口。该GET方法以及初始示例中调用的其他与 HTTP 方法相关的方法只是对该handle方法的包装。
该方法会根据的和相对路径参数handle计算绝对路径,同时调用方法来获取最终的数组。这些结果作为参数传递给的方法,用于注册处理函数。basePathRouterGroupcombineHandlershandlersaddRouteEngine
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复制到其中,再将参数 复制到其中,最后返回。也就是说,使用 注册任何方法时,实际结果都包含自身的。HandlersRouterGrouphandlersmergedHandlershandleHandlersRouterGroup
使用基数树加速路由检索
在官网提到的“Fast”特性点中,提到了基于基数树(Radix Tree)来实现网络请求的路由。这部分Gin并没有实现,而是httprouter在一开始介绍Gin时提到的,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
}
为每个 HTTP 方法维护Engine一个基数树,这棵树的根节点和方法名一起保存在一个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
}
可以看到,在addRoute的方法中Engine,会先使用 的get方法trees获取 对应的radix树的根节点method。如果没有获取到radix树的根节点,说明之前没有为此注册过方法method,则会创建一个树节点作为树的根节点,并添加到 中trees。
获取到根节点之后,再使用addRoute根节点的方法来handlers为路径注册一组处理函数path。这一步就是为 创建一个节点path,handlers并存储到radix树中。如果尝试注册一个已经注册过的地址,addRoute会直接抛出panic错误。
在处理HTTP请求的时候,需要通过 来找到对应节点的值path。根节点有一个getValue方法负责处理查询操作,我们在讲Gin处理HTTP请求的时候会提到这一点。
导入中间件处理函数
Use的方法可以RouterGroup导入一组中间件处理函数。官网提到的功能点中的“中间件支持”就是通过该Use方法实现的。
在最初的例子中,创建Engine结构体变量时,New并没有使用 ,而是Default使用了 extra 。我们来看看Defaultextra 到底起什么作用。
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方法,但只是调用了Use方法RouterGroup并进行了一些辅助操作。
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()
}
可以看出Use的方法也很简单,只是通过 把RouterGroup参数的中间件处理函数添加到自身中而已。Handlersappend
开始运行
在小例子中,最后一步调用了不带参数Run的方法Engine,调用之后整个框架就开始运行了,用浏览器访问注册的地址就可以正确触发回调。
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。
启动服务使用了标准库包ListenAndServe中的 方法。该方法接受一个监听地址和一个接口的变量。接口的定义很简单,只有一个方法。net/httpHandlerHandlerServeHTTP
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会负责接受并建立连接,而当连接上有数据时,则会ServeHTTP调用 的方法进行handler处理。
处理消息
ServeHTTP的这个是Engine处理消息的回调函数,我们来看一下它的内容。
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获取,然后将 绑定到回调函数的参数上,然后以为参数调用 方法来处理本次网络请求,最后将 放回池中。 我们先只看该方法的核心部分。ContextpoolContexthandleHTTPRequestContextContexthandleHTTPRequest
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本次处理,然后调用Next的函数Context执行 中的方法handlers。最后,将本次请求的返回数据写入 的responseWriter类型对象中Context。
语境
在处理 HTTP 请求时,所有与上下文相关的数据都在Context变量中。作者在Context结构体的注释中也写道“上下文是 gin 中最重要的部分”,可见其重要性。
在谈到ServeHTTP的方法时Engine,可以看出Context并不是直接创建的,而是通过变量Get的方法获取的。取出后,在使用前会重置其状态,使用完毕后会放回池中。 变量的类型为。目前只需知道它是 Go 官方提供的一个支持并发使用的对象池即可。你可以通过 的方法从池中获取对象,也可以使用 的方法将对象放入池中。当池为空并使用 的方法时,它会通过自己的方法创建一个对象并返回。 这个方法定义在的方法中。我们再来看看的方法。poolEnginepoolEnginesync.PoolGetPutGetNewNewNewEngineNewEngine
func New() *Engine {
//... Omit other code
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
从代码中可以看出,的创建方法Context就是allocateContext的方法Engine。该方法本身并没有什么玄机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}
}
Next上面提到的的方法会Context执行 中的所有方法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跟它的使用方式有很大关系,主要是为了配合一些中间件的功能。比如panic在某一个 的执行过程中触发了,就可以在中间件中handler捕获错误,然后再次调用继续执行后面的,而不会因为其中一个 的问题而影响到整个数组。recoverNexthandlershandlershandler
处理恐慌
在 Gin 中,如果某个请求的处理函数触发了panic,整个框架并不会直接崩溃,而是抛出一个错误信息,服务仍然会继续提供。这和 Lua 框架通常执行xpcall消息处理函数的方式有些类似。这个操作就是官方文档中提到的“Crash-free”特性点。
如上文所述,在使用gin.Default创建一个 时Engine,会执行Use的方法Engine,传入两个函数,其中一个是 函数的返回值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,然后执行错误处理。处理完成后,调用Next的方法Context,使得原本按顺序执行的handlers的可以继续执行。Context
Leapcell:用于 Web 托管、异步任务和 Redis 的下一代无服务器平台
最后给大家介绍一下部署Gin服务的最佳平台:Leapcell。
1.多语言支持
- 使用 JavaScript、Python、Go 或 Rust 进行开发。
2. 免费部署无限项目
- 仅按使用量付费 — 无请求,无费用。
3.无与伦比的成本效益
- 按使用量付费,无闲置费用。
- 例如:25 美元支持 694 万个请求,平均响应时间为 60 毫秒。
4. 简化的开发人员体验
- 直观的用户界面,轻松设置。
- 全自动 CI/CD 管道和 GitOps 集成。
- 实时指标和日志记录可提供可操作的见解。
5.轻松的可扩展性和高性能
- 自动扩展以轻松处理高并发。
- 零运营开销——只需专注于建设。
在文档中探索更多!
Leapcell Twitter:https://x.com/LeapcellHQ
文章来源:https://dev.to/leapcell/a-deep-dive-into-gin-golangs-leading-framework-5e39
后端开发教程 - Java、Spring Boot 实战 - msg200.com


