Gin 是 Go 中一款优秀的 Web 框架,相对于 Go 本身提供的 net/http 标准库,Gin框架具有以下优点:
- 更快速的开发:Gin 提供了许多实用的中间件和辅助函数,可以加速 Web 应用程序的开发过程。
- 更强大的路由功能:Gin 支持更灵活的路由定义,包括参数化路由、路由组,使用压缩前缀树进行存储,使得管理复杂的路由更加容易。
- 更丰富的中间件支持:Gin 内置了许多常用的中间件,如日志记录、错误恢复等,可以方便地扩展应用程序的功能。
快速入门
go
func main() {
r := gin.Default()
r.GET("/hello", func(context *gin.Context) {
context.JSON(200, gin.H{
"message":"hello world",
})
})
r.Run(":8090")
}
首先创建一个Gin 对象 r , 实际它本质是 Http Handler。
注册一个 HTTP 方法为 Get, path 为 /hello 的处理函数。
运行 Http 服务,端口号为 8090。
当在浏览器输入 http://127.0.0.1:8090/hello
后, 会返回一个字符串。
代码分析
我们根据上述示例中的代码进行分析。
首先创建一个对象 Engine
,它是 Gin 中的一个核心数据结构。
默认的 Engine 对象使用了 Logger 和 Recovery 中间件。
Logger 中间件主要是定义如何打印日志。
Recovery 中间件能够捕获 HTTP 请求处理过程中产生的所有 panic, 并返回 500 错误。
scss
func Default() *Engine {
engine := New()
engine.Use(Logger(), Recovery())
return engine
}
然后进入 New()
方法生成一个 Engine
对象。
Engine
对象中有三个比较重要的参数:RouterGroup ,pool ,trees。刚开始学习时可以只关注这三个参数,然后随着学习的深入再探究其他参数的作用。
创建好 Engine 对象后,我们就可以为不同 HTTP 方法的不同 path 请求创建对应的 handler,当服务器接收到该请求后,我们就可以找到对应的 handler 处理请求。
以上面列举的 Get /hello
请求样例进行分析。
方法入口为:
vbnet
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
其中:relativePath
值为 /hello
, handlers 为:
javascript
func(context *gin.Context) {
context.JSON(200, gin.H{
"message":"hello world",
}
在 group.handle
方法中,主要执行了三个逻辑:
在 //1
中,进行 path 拼接,将 RouterGroup
中的 basePath
和方法传入的 relativePath
进行进行组装,获取一个完整的 path。
从中我们可以发现,各种 handlers 都是在对应 RouterGroup
基础上进行实现的。
可以将共享相同特性的路由抽象到一个路由组 RouterGroup
中,提高代码重用性和维护性。
同时,可以轻松地为特定的路由组添加中间件,以便在请求到达路由处理函数之前或之后执行特定操作,从而简化了中间件的管理和使用。
csharp
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath) //1
handlers = group.combineHandlers(handlers) //2
group.engine.addRoute(httpMethod, absolutePath, handlers) //3
return group.returnObj()
}
在 //2
中,深拷贝 RouterGroup
中的 handlers 和注册传入的 handlers,组合成新的 handlers 数组。
在 //3
中,获取该 RouterGroup
所属的 Engine
对象,根据 HTTP方法类型,从方法路由树树组 trees
中找到对应的路由树,然后将该 path
的 handlers 注册到此路由树中。
下面就来到了重头戏部分,介绍是如何注册这些 handlers 到路由树的。
注册路由树
Gin 选择以压缩前缀树为基础实现路由树。
压缩前缀树又是以前缀树为基础。
前缀树有以下特点:
- 根节点不包含字符,其他节点都包含一个字符。
- 从根节点到某一个节点A,将对应路径的每个节点上的字符进行连接,即为该节点A对应的字符串。
- 尽可能复用公共前缀,非必要不会分配新的节点。
压缩前缀树是前缀树的优化版本。当某个节点只有一个子节点时,就会向上移动,与父节点合并。
Gin 为什么要使用压缩前缀树来实现路由树呢?
- 高效的路由匹配: 压缩前缀树可以实现高效的最长前缀匹配。当请求到达时,Gin 可以快速地从所有已注册的路由中找到最匹配的路由,以提高性能和响应速度。
- 动态路由支持:压缩前缀树天然支持动态路由,这使得 Gin 能够轻松处理带有参数的路由,而不需要复杂的额外逻辑。
- 灵活性和扩展性:通过使用压缩前缀树,Gin 能够更加灵活地管理路由规则。这种数据结构使得在添加、删除或者修改路由时更加高效,并且可以轻松地支持各种路由匹配规则。
介绍完一些前缀树知识点后,开始分析 Gin 是如何实现路由树的。
实现入口即为上面介绍的步骤3。
调用 group.engine.addRoute(httpMethod, absolutePath, handlers)
n 方法将 path 注册到对应的路由树上。
假设存在路由: 1)/users,2) /user/:id,3)/user/detail, 4) /undo, 对应的路由树如下:
从9个方法路由树中根据根据方法名(比如:GET, POST)找到对应的路由树。
如果为空时,创建新的一个路由树并添加到切片中。
调用 addRoute
方法添加 path。
ini
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)
......
addRoute
方法较为复杂,存在多种情况,我们以上述的路由规则为例进行分析。
1) 当路由树为空路由树,首次添加 path 时, 假设 path 为 /users
:
2)之前已经有路由树了,且存在公共前缀,公共部分比当前节点的 path 长度小。例如:存在节点 root, 该节点的 path 为 /users
, 新添加 path 为 /undo
, 它们的公共前缀为 /u
, 添加过程如下:
3)另一种常用的就是动态路由,有通配符:
,比如路由规则为 /users/:id
,其中 :id
为参数部分,可以匹配任意字符串。这种类型的路由通常用于捕获 URL 中的变量,并将其作为输入参数传递给相应的处理函数。假设已有路由规则:/users
, 要添加路由规则:/users/:id
:
实际还有其他情况,比如路由规则类似这种的,/user/*info
,在这里就不赘述了,只介绍下常见的情况。
在添加路由时,Gin 做了一些优化,会根据子节点列表中每个节点的 priority 值进行排序,将 priority 值高的节点排序到排到前面,方便查找时提前加速查到对应的路由节点。
至此介绍完了注册路由树的大体过程,当有请求过来时,是怎么查找到对用的路由节点呢?
检索路由树
要能处理请求,首先需要启动服务。调用 Engine.Run()
方法进行启动。
scss
func (engine *Engine) Run(addr ...string) (err error) {
........
address := resolveAddress(addr)
err = http.ListenAndServe(address, engine.Handler())
return
}
通过代码我们可以发现,Gin 最终会调用 http 下的 ListenAndServe
方法进行处理,也进一步证明了 Gin 是在 http 包基础上的进一步开发。
Gin 主要是将 Engine 作为一个 Handler 注册到 http 的 Serve 中。
http 为什么会识别出 Engine 是它的一个 Handler 呢?
这是因为 Handler 是一个 interface, 它定义了一个方法 ServeHTTP
:
go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Engine 实现了这个方法,所以就可以被看作 http 中的一个 Handler。
scss
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从对象池中获取一个 Context
c := engine.pool.Get().(*Context)
// 重置 Context
c.writermem.reset(w)
c.Request = req
c.reset()
// 处理 http 请求
engine.handleHTTPRequest(c)
// 处理完后,再把 Context 方法放回到对象池中
engine.pool.Put(c)
}
Engine 作为 http 的一个 Handler,每当 http 请求经过时,就会调用 ServeHTTP 方法进行处理。
- 每次请求时,会先从对象池中获取一个 Context
- 重置 Context
- 调用 handleHTTPRequest 处理 http 请求
- 处理完后把 Context 放回到对象池中
从中可以发现核心的处理逻辑为 handleHTTPRequest
方法。
检索路由节点的逻辑主要是在 root.getValue
中。