解析 Gin 框架底層原理
0 前言
上一週和大家聊了 Golang HTTP 標準庫的實現原理. 本週在此基礎上做個延伸,聊聊 web 框架 Gin 的底層實現原理.
1 Gin 與 HTTP
1.1 Gin 的背景
Gin 是 Golang 世界裏最流行的 web 框架,於 github 開源:https://github.com/gin-gonic/gin
其中本文涉及到的源碼走讀部分,代碼均取自 gin tag:v1.9.0 版本.
支撐研發團隊選擇 Gin 作爲 web 框架的原因包括:
-
• 支持中間件操作( handlersChain 機制 )
-
• 更方便的使用( gin.Context )
-
• 更強大的路由解析能力( radix tree 路由樹 )
括號中的知識點將在後文逐一介紹.
1.2 Gin 與 net/http 的關係
上週剛和大家一起探討了 Golang net/http 標準庫的實現原理,正是爲本篇內容的學習打下鋪墊. 沒看過的同學建議先回到上篇內容中熟悉一下.
Gin 是在 Golang HTTP 標準庫 net/http 基礎之上的再封裝,兩者的交互邊界如下圖:
可以看出,在 net/http 的既定框架下,gin 所做的是提供了一個 gin.Engine 對象作爲 Handler 注入其中,從而實現路由註冊 / 匹配、請求處理鏈路的優化.
1.3 Gin 框架使用示例
下面提供一段接入 Gin 的示例代碼,讓大家預先感受一下 Gin 框架的使用風格:
-
• 構造 gin.Engine 實例:gin.Default()
-
• 路由組註冊中間件:Engine.Use()
-
• 路由組註冊 POST 方法下的 handler:Engine.POST()
-
• 啓動 http server:Engine.Run()
import "github.com/gin-gonic/gin"
func main() {
// 創建一個 gin Engine,本質上是一個 http Handler
mux := gin.Default()
// 註冊中間件
mux.Use(myMiddleWare)
// 註冊一個 path 爲 /ping 的處理函數
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// 運行 http 服務
if err := mux.Run(":8080"); err != nil {
panic(err)
}
}
2 註冊 handler 流程
2.1 核心數據結構
首先看下核心數據結構:
(1)gin.Engine
type Engine struct {
// 路由組
RouterGroup
// ...
// context 對象池
pool sync.Pool
// 方法路由樹
trees methodTrees
// ...
}
// net/http 包下的 Handler interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// ...
}
Engine 爲 Gin 中構建的 HTTP Handler,其實現了 net/http 包下 Handler interface 的抽象方法: Handler.ServeHTTP,因此可以作爲 Handler 注入到 net/http 的 Server 當中.
Engine 包含的核心內容包括:
-
• 路由組 RouterGroup:第(2)部分展開
-
• Context 對象池 pool:基於 sync.Pool 實現,作爲複用 gin.Context 實例的緩衝池. gin.Context 的內容於本文第 5 章詳解
-
• 路由樹數組 trees:共有 9 棵路由樹,對應於 9 種 http 方法. 路由樹基於壓縮前綴樹實現,於本文第 4 章詳解.
9 種 http 方法展示如下:
const (
MethodGet = "GET"
MethodHead = "HEAD"
MethodPost = "POST"
MethodPut = "PUT"
MethodPatch = "PATCH" // RFC 5789
MethodDelete = "DELETE"
MethodConnect = "CONNECT"
MethodOptions = "OPTIONS"
MethodTrace = "TRACE"
)
(2)RouterGroup
type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}
RouterGroup 是路由組的概念,其中的配置將被從屬於該路由組的所有路由複用:
-
• Handlers:路由組共同的 handler 處理函數鏈. 組下的節點將拼接 RouterGroup 的公用 handlers 和自己的 handlers,組成最終使用的 handlers 鏈
-
• basePath:路由組的基礎路徑. 組下的節點將拼接 RouterGroup 的 basePath 和自己的 path,組成最終使用的 absolutePath
-
• engine:指向路由組從屬的 Engine
-
• root:標識路由組是否位於 Engine 的根節點. 當用戶基於 RouterGroup.Group 方法創建子路由組後,該標識爲 false
(3)HandlersChain
type HandlersChain []HandlerFunc
type HandlerFunc func(*Context)
HandlersChain 是由多個路由處理函數 HandlerFunc 構成的處理函數鏈. 在使用的時候,會按照索引的先後順序依次調用 HandlerFunc.
2.2 流程入口
下面以創建 gin.Engine 、註冊 middleware 和註冊 handler 作爲主線,進行源碼走讀和原理解析:
func main() {
// 創建一個 gin Engine,本質上是一個 http Handler
mux := gin.Default()
// 註冊中間件
mux.Use(myMiddleWare)
// 註冊一個 path 爲 /ping 的處理函數
mux.POST("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, "pone")
})
// ...
}
2.3 初始化 Engine
方法調用:gin.Default -> gin.New
-
• 創建一個 gin.Engine 實例
-
• 創建 Enging 的首個 RouterGroup,對應的處理函數鏈 Handlers 爲 nil,基礎路徑 basePath 爲 "/",root 標識爲 true
-
• 構造了 9 棵方法路由樹,對應於 9 種 http 方法
-
• 創建了 gin.Context 的對象池
路由樹相關的內容見本文第 4 章;gin.Context 有關內容見本文第 5 章.
func Default() *Engine {
engine := New()
// ...
return engine
}
func New() *Engine {
// ...
// 創建 gin Engine 實例
engine := &Engine{
// 路由組實例
RouterGroup: RouterGroup{
Handlers: nil,
basePath: "/",
root: true,
},
// ...
// 9 棵路由壓縮前綴樹,對應 9 種 http 方法
trees: make(methodTrees, 0, 9),
// ...
}
engine.RouterGroup.engine = engine
// gin.Context 對象池
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
2.3 註冊 middleware
通過 Engine.Use 方法可以實現中間件的註冊,會將註冊的 middlewares 添加到 RouterGroup.Handlers 中. 後續 RouterGroup 下新註冊的 handler 都會在前綴中拼上這部分 group 公共的 handlers.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
// ...
return engine
}
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
2.4 註冊 handler
以 http post 爲例,註冊 handler 方法調用順序爲 RouterGroup.POST-> RouterGroup.handle,接下來會完成三個步驟:
-
• 拼接出待註冊方法的完整路徑 absolutePath
-
• 拼接出代註冊方法的完整處理函數鏈 handlers
-
• 以 absolutePath 和 handlers 組成 kv 對添加到路由樹中
func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodPost, relativePath, handlers)
}
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()
}
(1)完整路徑拼接
結合 RouterGroup 中的 basePath 和註冊時傳入的 relativePath,組成 absolutePath
func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
return joinPaths(group.basePath, relativePath)
}
func joinPaths(absolutePath, relativePath string) string {
if relativePath == "" {
return absolutePath
}
finalPath := path.Join(absolutePath, relativePath)
if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' {
return finalPath + "/"
}
return finalPath
}
(2)完整 handlers 生成
深拷貝 RouterGroup 中 handlers 和註冊傳入的 handlers,生成新的 handlers 數組並返回
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
}
(3)註冊 handler 到路由樹
-
• 獲取 http method 對應的 methodTree
-
• 將 absolutePath 和對應的 handlers 註冊到 methodTree 中
路由註冊方法 root.addRoute 的信息量比較大,放在本文第 4 章中詳細拆解.
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)
// ...
}
3 啓動服務流程
3.1 流程入口
下面通過 Gin 框架運行 http 服務爲主線,進行源碼走讀:
func main() {
// 創建一個 gin Engine,本質上是一個 http Handler
mux := gin.Default()
// 一鍵啓動 http 服務
if err := mux.Run(); err != nil{
panic(err)
}
}
3.2 啓動服務
一鍵啓動 Engine.Run 方法後,底層會將 gin.Engine 本身作爲 net/http 包下 Handler interface 的實現類,並調用 http.ListenAndServe 方法啓動服務.
func (engine *Engine) Run(addr ...string) (err error) {
// ...
err = http.ListenAndServe(address, engine.Handler())
return
}
順便多提一嘴,ListenerAndServe 方法本身會基於主動輪詢 + IO 多路複用的方式運行,因此程序在正常運行時,會始終阻塞於 Engine.Run 方法,不會返回.
func (srv *Server) Serve(l net.Listener) error {
// ...
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, err := l.Accept()
// ...
connCtx := ctx
// ...
c := srv.newConn(rw)
// ...
go c.serve(connCtx)
}
}
3.3 處理請求
在服務端接收到 http 請求時,會通過 Handler.ServeHTTP 方法進行處理. 而此處的 Handler 正是 gin.Engine,其處理請求的核心步驟如下:
-
• 對於每筆 http 請求,會爲其分配一個 gin.Context,在 handlers 鏈路中持續向下傳遞
-
• 調用 Engine.handleHTTPRequest 方法,從路由樹中獲取 handlers 鏈,然後遍歷調用
-
• 處理完 http 請求後,會將 gin.Context 進行回收. 整個回收複用的流程基於對象池管理
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.handleHTTPRequest 方法核心步驟分爲三步:
-
• 根據 http method 取得對應的 methodTree
-
• 根據 path 從 methodTree 中找到對應的 handlers 鏈
-
• 將 handlers 鏈注入到 gin.Context 中,通過 Context.Next 方法按照順序遍歷調用 handler
此處根據 path 從路由樹尋找 handlers 的邏輯位於 root.getValue 方法中,和路由樹數據結構有關,放在本文第 4 章詳解;
根據 gin.Context.Next 方法遍歷 handler 鏈的內容放在本文第 5 章詳解.
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
// ...
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.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
// ...
break
}
// ...
}
4 Gin 的路由樹
4.1 策略與原理
在聊 Gin 路由樹實現原理之前,需要先補充一個壓縮前綴樹 radix tree 的基礎設定.
(1)前綴樹
前綴樹又稱 trie 樹,是一種基於字符串公共前綴構建索引的樹狀結構,核心點包括:
-
• 除根節點之外,每個節點對應一個字符
-
• 從根節點到某一節點,路徑上經過的字符串聯起來,即爲該節點對應的字符串
-
• 儘可能複用公共前綴,如無必要不分配新的節點
tries 樹在 leetcode 上的題號爲 208,大家感興趣不妨去刷刷算法題,手動實現一下.
(2)壓縮前綴樹
壓縮前綴樹又稱基數樹或 radix 樹,是對前綴樹的改良版本,優化點主要在於空間的節省,核心策略體現在:
倘若某個子節點是其父節點的唯一孩子,則與父節點進行合併
在 gin 框架中,是用壓縮前綴樹
(3)爲什麼使用壓縮前綴樹
與壓縮前綴樹相對的就是使用 hashmap,以 path 爲 key,handlers 爲 value 進行映射關聯,這裏選擇了前者的原因在於:
-
• path 匹配時不是完全精確匹配,比如末尾 ‘/’ 符號的增減、全匹配符號 '*' 的處理等,map 無法勝任(模糊匹配部分的代碼於本文中並未體現,大家可以深入源碼中加以佐證)
-
• 路由的數量相對有限,對應數量級下 map 的性能優勢體現不明顯,在小數據量的前提下,map 性能甚至要弱於前綴樹
-
• path 串通常存在基於分組分類的公共前綴,適合使用前綴樹進行管理,可以節省存儲空間
(4)補償策略
在 Gin 路由樹中還使用一種補償策略,在組裝路由樹時,會將註冊路由句柄數量更多的 child node 擺放在 children 數組更靠前的位置.
這是因爲某個鏈路註冊的 handlers 句柄數量越多,一次匹配操作所需要話費的時間就越長,被匹配命中的概率就越大,因此應該被優先處理.
4.2 核心數據結構
下面聊一下路由樹的數據結構,對應於 9 種 http method,共有 9 棵 methodTree. 每棵 methodTree 會通過 root 指向 radix tree 的根節點.
type methodTree struct {
method string
root *node
}
node 是 radix tree 中的節點,對應節點含義如下:
-
• path:節點的相對路徑,拼接上 RouterGroup 中的 basePath 作爲前綴後才能拿到完整的路由 path
-
• indices:由各個子節點 path 首字母組成的字符串,子節點順序會按照途徑的路由數量 priority 進行排序
-
• priority:途徑本節點的路由數量,反映出本節點在父節點中被檢索的優先級
-
• children:子節點列表
-
• handlers:當前節點對應的處理函數鏈
type node struct {
// 節點的相對路徑
path string
// 每個 indice 字符對應一個孩子節點的 path 首字母
indices string
// ...
// 後繼節點數量
priority uint32
// 孩子節點列表
children []*node
// 處理函數鏈
handlers HandlersChain
// path 拼接上前綴後的完整路徑
fullPath string
}
4.3 註冊到路由樹
承接本文 2.4 小節第(3)部分,下述代碼展示了將一組 path + handlers 添加到 radix tree 的詳細過程,核心位置均已給出註釋,此處就不再贅述了,請大家盡情享用源碼盛宴吧!
// 插入新路由
func (n *node) addRoute(path string, handlers HandlersChain) {
fullPath := path
// 每有一個新路由經過此節點,priority 都要加 1
n.priority++
// 加入當前節點爲 root 且未註冊過子節點,則直接插入由並返回
if len(n.path) == 0 && len(n.children) == 0 {
n.insertChild(path, fullPath, handlers)
n.nType = root
return
}
// 外層 for 循環斷點
walk:
for {
// 獲取 node.path 和待插入路由 path 的最長公共前綴長度
i := longestCommonPrefix(path, n.path)
// 倘若最長公共前綴長度小於 node.path 的長度,代表 node 需要分裂
// 舉例而言:node.path = search,此時要插入的 path 爲 see
// 最長公共前綴長度就是 2,len(n.path) = 6
// 需要分裂爲 se -> arch
-> e
if i < len(n.path) {
// 原節點分裂後的後半部分,對應於上述例子的 arch 部分
child := node{
path: n.path[i:],
// 原本 search 對應的參數都要託付給 arch
indices: n.indices,
children: n.children,
handlers: n.handlers,
// 新路由 see 進入時,先將 search 的 priority 加 1 了,此時需要扣除 1 並賦給 arch
priority: n.priority - 1,
fullPath: n.fullPath,
}
// 先建立 search -> arch 的數據結構,後續調整 search 爲 se
n.children = []*node{&child}
// 設置 se 的 indice 首字母爲 a
n.indices = bytesconv.BytesToString([]byte{n.path[i]})
// 調整 search 爲 se
n.path = path[:i]
// search 的 handlers 都託付給 arch 了,se 本身沒有 handlers
n.handlers = nil
// ...
}
// 最長公共前綴長度小於 path,正如 se 之於 see
if i < len(path) {
// path see 扣除公共前綴 se,剩餘 e
path = path[i:]
c := path[0]
// 根據 node.indices,輔助判斷,其子節點中是否與當前 path 還存在公共前綴
for i, max := 0, len(n.indices); i < max; i++ {
// 倘若 node 子節點還與 path 有公共前綴,則令 node = child,並調到外層 for 循環 walk 位置開始新一輪處理
if c == n.indices[i] {
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// node 已經不存在和 path 再有公共前綴的子節點了,則需要將 path 包裝成一個新 child node 進行插入
// node 的 indices 新增 path 的首字母
n.indices += bytesconv.BytesToString([]byte{c})
// 把新路由包裝成一個 child node,對應的 path 和 handlers 會在 node.insertChild 中賦值
child := &node{
fullPath: fullPath,
}
// 新 child node append 到 node.children 數組中
n.addChild(child)
n.incrementChildPrio(len(n.indices) - 1)
// 令 node 指向新插入的 child,並在 node.insertChild 方法中進行 path 和 handlers 的賦值操作
n = child
n.insertChild(path, fullPath, handlers)
return
}
// 此處的分支是,path 恰好是其與 node.path 的公共前綴,則直接複製 handlers 即可
// 例如 se 之於 search
if n.handlers != nil {
panic("handlers are already registered for path '" + fullPath + "'")
}
n.handlers = handlers
// ...
return
}
func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) {
// ...
n.path = path
n.handlers = handlers
// ...
}
呼應於 4.1 小節第(4)部分談到的補償策略,下面這段代碼體現了,在每個 node 的 children 數組中,child node 在會依據 priority 有序排列,保證 priority 更高的 child node 會排在數組前列,被優先匹配.
func (n *node) incrementChildPrio(pos int) int {
cs := n.children
cs[pos].priority++
prio := cs[pos].priority
// Adjust position (move to front)
newPos := pos
for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- {
// Swap node positions
cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1]
}
// Build new index char string
if newPos != pos {
n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty
n.indices[pos:pos+1] + // The index char we move
n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos'
}
return newPos
}
4.4 檢索路由樹
承接本文 3.3 小節,下述代碼展示了從路由樹中匹配 path 對應 handler 的詳細過程,請大家結合註釋消化源碼吧.
type nodeValue struct {
// 處理函數鏈
handlers HandlersChain
// ...
}
// 從路由樹中獲取 path 對應的 handlers
func (n *node) getValue(path string, params *Params, skippedNodes *[]skippedNode, unescape bool) (value nodeValue) {
var globalParamsCount int16
// 外層 for 循環斷點
walk:
for {
prefix := n.path
// 待匹配 path 長度大於 node.path
if len(path) > len(prefix) {
// node.path 長度 < path,且前綴匹配上
if path[:len(prefix)] == prefix {
// path 取爲後半部分
path = path[len(prefix):]
// 遍歷當前 node.indices,找到可能和 path 後半部分可能匹配到的 child node
idxc := path[0]
for i, c := range []byte(n.indices) {
// 找到了首字母匹配的 child node
if c == idxc {
// 將 n 指向 child node,調到 walk 斷點開始下一輪處理
n = n.children[i]
continue walk
}
}
// ...
}
}
// 倘若 path 正好等於 node.path,說明已經找到目標
if path == prefix {
// ...
// 取出對應的 handlers 進行返回
if value.handlers = n.handlers; value.handlers != nil {
value.fullPath = n.fullPath
return
}
// ...
}
// 倘若 path 與 node.path 已經沒有公共前綴,說明匹配失敗,會嘗試重定向,此處不展開
// ...
}
5 Gin.Context
5.1 核心數據結構
gin.Context 的定位是對應於一次 http 請求,貫穿於整條 handlersChain 調用鏈路的上下文,其中包含了如下核心字段:
-
• Request/Writer:http 請求和響應的 reader、writer 入口
-
• handlers:本次 http 請求對應的處理函數鏈
-
• index:當前的處理進度,即處理鏈路處於函數鏈的索引位置
-
• engine:Engine 的指針
-
• mu:用於保護 map 的讀寫互斥鎖
-
• Keys:緩存 handlers 鏈上共享數據的 map
type Context struct {
// ...
// http 請求參數
Request *http.Request
// http 響應 writer
Writer ResponseWriter
// ...
// 處理函數鏈
handlers HandlersChain
// 當前處於處理函數鏈的索引
index int8
engine *Engine
// ...
// 讀寫鎖,保證併發安全
mu sync.RWMutex
// key value 對存儲 map
Keys map[string]any
// ..
}
5.2 複用策略
gin.Context 作爲處理 http 請求的通用數據結構,不可避免地會被頻繁創建和銷燬. 爲了緩解 GC 壓力,gin 中採用對象池 sync.Pool 進行 Context 的緩存複用,處理流程如下:
-
• http 請求到達時,從 pool 中獲取 Context,倘若池子已空,通過 pool.New 方法構造新的 Context 補上空缺
-
• http 請求處理完成後,將 Context 放回 pool 中,用以後續複用
sync.Pool 並不是真正意義上的緩存,將其稱爲回收站或許更加合適,放入其中的數據在邏輯意義上都是已經被刪除的,但在物理意義上數據是仍然存在的,這些數據可以存活兩輪 GC 的時間,在此期間倘若有被獲取的需求,則可以被重新複用.
和對象池 sync.Pool 有關的內容可以閱讀我的文章 《Golang 協程池 Ants 實現原理》,其中有將對象池作爲協程池的前置知識點,進行詳細講解.
type Engine struct {
// context 對象池
pool sync.Pool
}
func New() *Engine {
// ...
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
func (engine *Engine) allocateContext(maxParams uint16) *Context {
v := make(Params, 0, maxParams)
// ...
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
5.3 分配與回收時機
gin.Context 分配與回收的時機是在 gin.Engine 處理 http 請求的前後,位於 Engine.ServeHTTP 方法當中:
-
• 從池中獲取 Context
-
• 重置 Context 的內容,使其成爲一個空白的上下文
-
• 調用 Engine.handleHTTPRequest 方法處理 http 請求
-
• 請求處理完成後,將 Context 放回池中
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)
}
5.4 使用時機
(1)handlesChain 入口
在 Engine.handleHTTPRequest 方法處理請求時,會通過 path 從 methodTree 中獲取到對應的 handlers 鏈,然後將 handlers 注入到 Context.handlers 中,然後啓動 Context.Next 方法開啓 handlers 鏈的遍歷調用流程.
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
}
// ...
}
// ...
}
(2)handlesChain 遍歷調用
推進 handlers 鏈調用進度的方法正是 Context.Next. 可以看到其中以 Context.index 爲索引,通過 for 循環依次調用 handlers 鏈中的 handler.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
由於 Context 本身會暴露於調用鏈路中,因此用戶可以在某個 handler 中通過手動調用 Context.Next 的方式來打斷當前 handler 的執行流程,提前進入下一個 handler 的處理中.
由於此時本質上是一個方法壓棧調用的行爲,因此在後置位 handlers 鏈全部處理完成後,最終會回到壓棧前的位置,執行當前 handler 剩餘部分的代碼邏輯.
結合下面的代碼示例來說,用戶可以在某個 handler 中,於調用 Context.Next 方法的前後分別聲明前處理邏輯和後處理邏輯,這裏的 “前” 和“後”相對的是後置位的所有 handler 而言.
func myHandleFunc(c *gin.Context){
// 前處理
preHandle()
c.Next()
// 後處理
postHandle()
}
此外,用戶可以在某個 handler 中通過調用 Context.Abort 方法實現 handlers 鏈路的提前熔斷.
其實現原理是將 Context.index 設置爲一個過載值 63,導致 Next 流程直接終止. 這是因爲 handlers 鏈的長度必須小於 63,否則在註冊時就會直接 panic. 因此在 Context.Next 方法中,一旦 index 被設爲 63,則必然大於整條 handlers 鏈的長度,for 循環便會提前終止.
const abortIndex int8 = 63
func (c *Context) Abort() {
c.index = abortIndex
}
此外,用戶還可以通過 Context.IsAbort 方法檢測當前 handlerChain 是出於正常調用,還是已經被熔斷.
func (c *Context) IsAborted() bool {
return c.index >= abortIndex
}
註冊 handlers,倘若 handlers 鏈長度達到 63,則會 panic
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
// 斷言 handlers 鏈長度必須小於 63
assert1(finalSize < int(abortIndex), "too many handlers")
// ...
}
(3)共享數據存取
gin.Context 作爲 handlers 鏈的上下文,還提供對外暴露的 Get 和 Set 接口向用戶提供了共享數據的存取服務,相關操作都在讀寫鎖的保護之下,能夠保證併發安全.
type Context struct {
// ...
// 讀寫鎖,保證併發安全
mu sync.RWMutex
// key value 對存儲 map
Keys map[string]any
}
func (c *Context) Get(key string) (value any, exists bool) {
c.mu.RLock()
defer c.mu.RUnlock()
value, exists = c.Keys[key]
return
}
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
if c.Keys == nil {
c.Keys = make(map[string]any)
}
c.Keys[key] = value
}
6 總結
對全文內容做個總結回顧:
-
• gin 將 Engine 作爲 http.Handler 的實現類進行注入,從而融入 Golang net/http 標準庫的框架之內
-
• gin 中基於 handler 鏈的方式實現中間件和處理函數的協調使用
-
• gin 中基於壓縮前綴樹的方式作爲路由樹的數據結構,對應於 9 種 http 方法共有 9 棵樹
-
• gin 中基於 gin.Context 作爲一次 http 請求貫穿整條 handler chain 的核心數據結構
-
• gin.Context 是一種會被頻繁創建銷燬的資源對象,因此使用對象池 sync.Pool 進行緩存複用
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/x8i9HvAzIHNbHCryLw5icg