Gin 是一个用 Go (Golang) 编写的 HTTP 框架。它的 API 类似于 Martini,但其性能比 Martini 快 40 倍。如果你需要高性能,试试 Gin 吧。
Gin的官方网站描述为一个具有“高性能”和“高生产效率”的web框架。它还提到了另外两个库。第一个是Martini,这也是一款web框架,并且名字像一种酒。虽然Gin使用了Martini的API,但它的运行速度比Martini快40倍。使用httprouter
是它比Martini快40倍的重要原因之一。
在官方网站上的“功能”部分,列出了八个关键特性,我们将在接下来的内容中逐步了解这些特点的具体实现。
- 快速
- 中间件支持
- 不会崩溃
- JSON验证功能
- 路由分组功能
- 错误管理
- 内置且可扩展的渲染功能
我们来看一看这里给出的最简单的例子,这里指的是Gin的官方文档(https://gin-gonic.com/docs/)中的例子。
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() // 监听并在 0.0.0.0:8080 上运行
}
全屏模式 退出全屏
运行这个示例,然后使用浏览器访问http://localhost:8080/ping
(这是一个本地服务器的URL),你会看到返回一个"pong"。
这个示例非常简单,它只需要三个简单的步骤。
- 使用
gin.Default()
来创建一个默认配置的Engine
对象。 - 在
Engine
的GET
方法上为 "/ping" 地址注册一个回调函数,该函数将返回 "pong"。 - 启动
Engine
来监听端口并提供服务。
从上面的小例子中的GET
方法可以看出,在Gin中,HTTP方法的处理函数需要使用相应的方法进行注册。
有九个HTTP方法,其中最常用的四个分别是GET
、POST
、PUT
和DELETE
,分别对应查询、插入、更新和删除四种操作。值得注意的是,Gin还提供了Any
接口,可以直接将所有HTTP方法的处理函数绑定到同一个地址。
返回的结果通常包含两到三个部分:code
和message
是必备的,而data
则根据需要包含附加数据。若无额外数据返回,data
部分可省略。例如,返回的code
值为200,message
值为"pong"。
在上面的例子中,我们使用了 gin.Default()
来创建 Engine
。实际上,gin.Default()
是 New
的一个封装,Engine
通过 New
创建。
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
//... 初始化 RouterGroup 的各项
},
//... 初始化剩余各项
}
engine.RouterGroup.engine = engine // 将 engine 的指针保存到 RouterGroup 中
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
全屏模式 退出全屏
先暂时简单看一下这个创建过程,暂时不用关注Engine
结构中各种成员变量的意义。可以看出,除了创建并初始化一个类型为Engine
的engine
变量之外,New
还将engine.pool.New
设为一个调用engine.allocateContext()
的匿名函数。这个函数的具体作用稍后会讨论。
在 Engine
中包含了一个嵌入的 RouterGroup
结构体。Engine
中与 HTTP 方法相关的接口都继承自 RouterGroup
。在官网的特性说明中提到的“路由分组”功能是通过 RouterGroup
结构体来实现的。
type RouterGroup struct {
Handlers HandlersChain // 本组的处理函数
basePath string // 关联路径
engine *Engine // 关联引擎
root bool // 根标志,仅默认创建于 Engine 的为 true
}
进入全屏 / 退出全屏
每个 RouterGroup
都会关联一个基础路径 basePath
。内嵌于 Engine
中的 RouterGroup
的 basePath
为 "/"。
此外,有一组处理函数 Handlers
。所有与此组关联的路径下的请求都将额外执行该组的处理函数,这些处理函数主要用于中间件调用。在创建 Engine
时,Handlers
会默认为 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) 合并处理程序(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "处理程序数量过多")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
全屏模式,退出全屏
combineHandlers
方法的作用是创建一个名为 mergedHandlers
的切片,首先将 RouterGroup
本身的 Handlers
复制到 mergedHandlers
,然后将参数中的 handlers
也复制进去,最后返回这个合并后的 mergedHandlers
。也就是说,在使用 handle
注册任何方法时,实际结果中包含了 RouterGroup
本身的 Handlers
。
在官方网站上提到的“快速”功能点中,提到网络请求路由是基于基数树(又称Radix Tree)实现的。这一部分不是 Gin 实现的,而是由 Gin 引入时提到的 httprouter
实现的。Gin 使用 httprouter
来实现这一部分功能。基数树的实现细节暂不讨论,我们目前只关注它的使用。以后可能会专门写一篇文章来介绍基数树的实现。
在 Engine
中有一个名为 trees
的变量,它是一个 methodTree
结构体的切片。这个变量保存了所有基数树的引用。
type methodTree struct {
method string // 方法的名字
root *node // 指向链表根节点的指针,
}
全屏按钮,按一下进入,再按一下退出
Engine
为每个 HTTP 方法维护一棵 Radix 树。这棵树的根节点和 HTTP 方法的名称一起保存在每个 methodTree
变量中。所有这些 methodTree
变量都被存储在 trees
中。
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
// 有些代码被省略了
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)
// 有些代码被省略了
}
进入全屏,退出全屏
可以看出,在Engine
的addRoute
方法中,它会先通过trees
的get
方法获取对应于method
的基数树的根节点。如果未能获取基数树的根节点,说明之前没有为该method
注册过任何方法,此时会创建一个新的树节点作为基数树的根节点,并将其添加到trees
中。
获取到根节点后,会使用根节点的addRoute
方法为路径path
注册一组处理函数handlers
。这一步骤是为path
和handlers
创建一个节点,并将其存储在基数树中。如果尝试为已注册的地址再次注册,addRoute
会直接抛出一个panic
错误。
在处理HTTP请求时,需要通过path
找到对应的节点值。根节点的getValue
方法负责处理查询操作。在讨论Gin如何处理HTTP请求时,我们还会提到这一点。
RouterGroup
的 Use
方法可以引入一系列中间件处理函数。官网提到的“中间件支持”功能是通过 Use
方法实现的。
在最初的示例中,在创建 Engine
结构体变量时,使用的不是 New
,而是 Default
,让我们来看看 Default
还做了什么额外的工作。
func Default() *Engine {
debugPrintWARNINGDefault() // 打印警告日志
engine := New() // 新建引擎
engine.Use(Logger(), Recovery()) // 使用中间件 Logger 和 Recovery
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() // 重建404处理程序
engine.rebuild405Handlers() // 重建405处理程序
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) {
//... 省略了某些代码
address := resolveAddress(addr) // 解析地址,默认地址是0.0.0.0:8080
debugPrint("正在监听%s上的HTTP请求并提供服务\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
进入全屏 退出全屏
Run
方法主要做两件事:解析地址和启动服务。但实际上,地址只需要传递一个字符串,为了兼容传递或不传递的情况,采用了可变参数。resolveAddress
方法会根据不同的 addr
情况处理结果。
服务启动时会调用标准库中的 net/http
包的 ListenAndServe
方法。该方法接受一个监听地址以及一个 Handler
接口类型的参数。Handler
接口定义相当简单,仅包含一个 ServeHTTP
方法。
函数 ListenAndServe 接收一个字符串 addr 和一个处理器 handler 作为参数,创建一个服务器对象,并调用其 ListenAndServe 方法。类型 Handler 是一个接口,定义了 ServeHTTP 方法。
进入全屏 退出全屏
由于 Engine
实现了 ServeHTTP
接口,因此当调用 ListenAndServe
方法时会将 Engine
本身传递进去。当检测到监视端口上有新的连接时,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
和 req
。第一个用来接收请求的回复。将回复数据写入 w
,以完成请求的响应。另一个是 req
,它保存了此请求的数据。所有后续处理需要的数据都可以从此 req
中读取。
ServeHTTP
方法做了四件事。首先,从 pool
中获取一个 Context
,然后将该 Context
绑定到回调函数的参数上。接着用该 Context
作为参数调用 handleHTTPRequest
方法来处理此网络请求。最后将该 Context
放回 pool
。
我们先只关注 handleHTTPRequest
方法的核心内容。
func (engine *Engine) handleHTTPRequest(c *Context) {
//... 跳过一些代码
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// 查找树中的路由
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
//... 跳过一些代码
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//... 跳过一些代码
}
//... 跳过一些代码
}
点击全屏按钮进入全屏模式,点击退出按钮退出全屏模式
handleHTTPRequest
方法主要做两件事。首先,根据请求的地址从前缀树中获取之前注册的方法。在这个过程中,将 handlers
赋值给当前的 Context
,然后调用 Context
的 Next
函数来执行 handlers
。最后,将此请求的返回数据写入 Context
中的 responseWriter
对象中。
处理HTTP请求时,所有与上下文相关的数据都存放在Context
变量中。作者也在Context
结构体的注释中写道“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 {
// 以下是省略的其他代码
engine.pool.New = func() any {
返回 engine.allocateContext()
}
return engine
}
全屏切换
从代码中可以看出,Context
的创建方法是Engine
的allocateContext
方法。allocateContext
方法没有什么特别之处,它先预分配了切片的长度,再创建对象并返回。
func (引擎 *Engine) 分配上下文() *Context {
参数 := make(参数, 0, 引擎.maxParams)
跳过节点 := make([]跳过节点, 0, 引擎.maxSections)
return &Context{引擎: 引擎, 参数: &参数, 跳过节点: &跳过节点}
}
点击进入全屏 点击退出全屏
之前提到的 Context
的 Next
方法会执行所有 handlers
中的方法。我们来看看它的实现吧。
func (c *Context) Next() {
c.index++
// 循环遍历处理器列表,直到当前索引超出范围
for c.index < int8(len(c.handlers)) {
// 调用当前索引对应的处理器
c.handlers[c.index](https://dev.to/c)
c.index++
}
}
全屏 退出全屏
尽管 handlers
是一个切片,但 Next
方法并不是简单地遍历 handlers
,而是引入了一个处理进度索引 index
,记录从 0 开始并根据每次方法的调用及完成情况进行递增。
Next
的设计与其使用紧密联系,主要是与某些中间件功能协同工作。例如,当某个handler
执行时触发了panic
,可以利用中间件中的recover
来捕获此错误,之后再次调用Next
继续执行后续的handler
,从而不会因单个handler
的问题而中断整个handlers
链的执行。
在 Gin 中,如果某个请求的处理函数触发了一个 panic
,整个框架不会直接崩溃。相反,会抛出一个错误消息,服务将继续提供。这与 Lua 框架通常使用 xpcall
来执行消息处理中的函数的方式有些类似。这一操作就是官方文档中提到的“无崩溃(Crash-free)”功能点。
正如前面提到的,当使用 gin.Default
创建 Engine
时,Engine
的 Use
方法会被执行来导入两个函数,其中一个函数是 Recovery
函数返回的一个对其他函数进行封装的函数。最终调用的是 CustomRecoveryWithWriter
函数。我们来看一下这个函数的实现。
func 带有输出流的自定义恢复处理函数(out io.Writer, handle RecoveryFunc) HandlerFunc {
//... 省略其他代码...
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
//... 处理错误的代码...
}
}()
c.Next() // 执行下一个处理器
}
}
切换到全屏, 退出全屏
这里我们不关注错误处理的细节,只看看它做了什么。这个函数返回一个匿名函数。在这个匿名函数里,使用defer
注册了另一个匿名函数。在内部的匿名函数中,通过recover
捕获panic
并进行错误处理。处理完成后,调用Context
的Next
方法,这样原先按顺序执行的Context
中的handlers
就可以继续执行了。
最后,我想介绍部署Gin服务的最佳平台是:Leapcell。
1. 支持多种语言
- 使用 JavaScript、Python、Go 或 Rust 进行开发。
2. 免费无限制地部署项目
- 只按需付费 — 没有请求,没有费用。
3. 无可比拟的成本效益
- 按使用量付费,无闲置时长费用。
- 例如,$25可以支持6.94M请求,平均响应时间60毫秒。
4. 精简的开发者体验
- 直观的用户界面,让设置变得轻松。
- 完全自动化的CI/CD管道和GitOps集成,让操作更加便捷。
- 实时度量和日志记录,帮助您获得可操作的见解。
5. 轻松的可扩展性和高性能表现
- 自动扩展,轻松应对高并发。
- 零运维负担,只需专注于创建。
了解更多请看资料!
Leapcell 推特: https://x.com/LeapcellHQ