深入探究 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
,官网提到的功能点中的“路由分组”就是通过该RouterGroup
struct实现的。
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
计算绝对路径,同时调用方法来获取最终的数组。这些结果作为参数传递给的方法,用于注册处理函数。basePath
RouterGroup
combineHandlers
handlers
addRoute
Engine
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
复制到其中,再将参数 复制到其中,最后返回。也就是说,使用 注册任何方法时,实际结果都包含自身的。Handlers
RouterGroup
handlers
mergedHandlers
handle
Handlers
RouterGroup
使用基数树加速路由检索
在官网提到的“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 。我们来看看Default
extra 到底起什么作用。
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
参数的中间件处理函数添加到自身中而已。Handlers
append
开始运行
在小例子中,最后一步调用了不带参数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/http
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
会负责接受并建立连接,而当连接上有数据时,则会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
获取,然后将 绑定到回调函数的参数上,然后以为参数调用 方法来处理本次网络请求,最后将 放回池中。 我们先只看该方法的核心部分。Context
pool
Context
handleHTTPRequest
Context
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
本次处理,然后调用Next
的函数Context
执行 中的方法handlers
。最后,将本次请求的返回数据写入 的responseWriter
类型对象中Context
。
语境
在处理 HTTP 请求时,所有与上下文相关的数据都在Context
变量中。作者在Context
结构体的注释中也写道“上下文是 gin 中最重要的部分”,可见其重要性。
在谈到ServeHTTP
的方法时Engine
,可以看出Context
并不是直接创建的,而是通过变量Get
的方法获取的。取出后,在使用前会重置其状态,使用完毕后会放回池中。 变量的类型为。目前只需知道它是 Go 官方提供的一个支持并发使用的对象池即可。你可以通过 的方法从池中获取对象,也可以使用 的方法将对象放入池中。当池为空并使用 的方法时,它会通过自己的方法创建一个对象并返回。 这个方法定义在的方法中。我们再来看看的方法。pool
Engine
pool
Engine
sync.Pool
Get
Put
Get
New
New
New
Engine
New
Engine
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
捕获错误,然后再次调用继续执行后面的,而不会因为其中一个 的问题而影响到整个数组。recover
Next
handlers
handlers
handler
处理恐慌
在 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