Gin 框架深度分析
Gin 是一個採用 Go 語言實現的 HTTP web 框架,提供了類似 Martini 的 API,但是性能遠強於 Martini,峯值性能是 Martini 的 40 倍。如果我們的項目需要高性能,毫無疑問採用 Gin。
Gin 官網列舉了該項目的 8 個如下關鍵特性:
-
高性能
-
中間件支持
-
防崩潰機制
-
JSON 校驗
-
路由分組
-
錯誤管理
-
內容渲染
示例 Demo
下面的 Demo 程序來自官方文檔,構建並運行該程序,然後在瀏覽器輸入 http://localhost:8080/ping,顯示字符串 {"message":"pong"}。
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
}
可以看到,使用 Gin 框架啓動一個 web 服務非常簡潔,只需三步操作:
-
使用
gin.Default()創建一個缺省的 Engine 對象。 -
在 Get 方法上註冊一個 / ping 路由處理函數。
-
啓動 Engine。
HTTP 方法
在 Gin 框架中,所有 HTTP 方法的處理函數需要在對應的 Method 上註冊。即r.Get註冊 Get 請求上的處理函數。HTTP 協議定義了 9 種請求方法。最常用的方法 GET、POST、PUT 和 DELETE 分別對應查、增、改和刪除操作,此外 Gin 還提供了 Any 接口,可以一次性將全部 HTTP 方法綁定到一個路由上。
返回內容包含 3 部分,其中狀態碼 (code) 和消息 (message) 必選,數據(data)爲非必選,用於承載額外的業務數據。如果沒有額外數據需要返回,可忽略 data 字段。上述示例中,code 爲 200,message 爲 pong。
創建 Engine 對象
在前面的示例程序中,通過gin.Default創建一個 Engine 對象,實際上該方法只做了一個簡單的包裝,內部調用的是 New 函數。與此同時,爲 engine.pool.New 設置初始化函數,供 engine.allocateContext 創建上下文對象,詳細分析見下文。
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
}
註冊路由回調函數
結構體 RouterGroup 以嵌入形式存在於 Engine 中,因此 Engine 繼承了 RouterGroup 實現的方法。
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,即 URL 參數,對於 Engine 來說,它的 basePath 是 /。Handlers 爲要執行的函數集合,匹配上 basePath 的 URL 會執行 Handlers 中的所有處理函數, 當然中間件函數也在 Handlers 中。
創建 Engine 對象時,Handlers 設置爲空 nil,我們可以使用 Use 方法往裏面註冊處理函數。
RouterGroup 的 handle 方法是所有 HTTP 註冊回調中最後的入口函數。
r.Get 方法調用的是 RouterGroup 的 Get 方法,該方法會調用 handle 註冊處理函數。
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
RouterGroup 的 handle 方法實現如下,主要有三點。一是計算絕對的 path,即完整的 URL。二是處理函數合併。三是將路由添加到 engine 中。
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()
}
combineHandlers 方法核心就是幹一件事:處理函數合併。申請一個新的函數切片,並按 finalSize 大小進行初始,然後將 group 中的 handler 和新註冊的 handler 一起合併到 mergedHandlers 返回。
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
}
採用基數數 (Radix Tree) 實現路由查詢加速
注意通過 Radix Tree 實現加速,是引入 httprouter 庫實現的,本文聚集於應用層面,不深入分析基數樹的底層實現。
在結構體 Engine 中,trees 是一個核心字段,用於維護基數樹的引用,該字段本質是 methodTree 結構體切片。
type methodTrees []methodTree
type methodTree struct {
method string
root *node
}
Engine 爲每種 HTTP 請求方法維護着一個基數樹。基數樹的頭對應 methodTree 結構的 root 字段,請求方法對應 methodTree 中的 method 字段。
Engine 的 addRoute 方法將路由處理函數添加到 Engine 的 trees 中,結合下面代碼,可見第一步就是從 engine.trees 中獲取到對應 method 的基數樹。如果基數樹不存在,則說明是首次添加對應的 method,將創建一個基數樹添加到 engine.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 方法將路由和處理函數註冊到基數樹上。具體來說,創建一個新的節點,節點的 path 和 handler 分別爲 addRoute 的入參。若嘗試註冊已存在的地址,addRoute 將直接拋出 panic 異常。
當處理 HTTP 請求時,調用 getValue 方法通過路徑在基數樹中找到對應節點的值。
添加中間件函數
使用 RouterGroup 的 Use 方法可以添加中間件處理函數。Engine 默認實例化函數中,使用 Use 方法默認註冊了日誌和異常處理中間件函數,代碼如下。
func Default() *Engine {
debugPrintWARNINGDefault() // Output log
engine := New() // Create object
engine.Use(Logger(), Recovery()) // Import middleware processing functions
return engine
}
Logger 函數的返回值也是一個函數,函數簽名爲 type HandlerFunc func(*Context),默認將程序日誌輸出到標準輸出中。Recovery 函數處理程序引發的 panic 問題。
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
儘管 Engine 中內嵌的 RouterGroup 實現了 Use 方法,但 Engine 本身也實現了 Use 方法,實現代碼如下。
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
RouterGroup 的 Use 方法實現邏輯非常簡單,就是將處理函數添加到自己的 Handlers 中。
func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
啓動 Engine
調用 Engine 的 Run 方法即運行它,Run 方法接收可變參數。我們也可以什麼都不傳,像本文開頭的例子,默認監聽在 0.0.0.0:8080 上。
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 方法負責處理地址解析工作。啓動服務使用標準庫 net/http 包中的 ListenAndServer 方法,該方法接受兩個參數,參數 1 爲監聽地址,參數 2 爲實現了 Handler 接口的實例。
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
因爲 Engine 實現了 ServeHTTP 接口,所以它可以傳遞給 ListenAndServe。當有新連接時,ListenAndServe 負責接受並建立連接,接收到連接上的數據後,調用處理器的 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)
}
ServeHTTP 方法參數 w 用於處理 HTTP 請求響應,參數 req 用於讀取本次處理的數據。整個 ServeHTTP 方法執行下面四個操作:
-
獲取上下文:從連接池 pool 中獲取 Context 對象
-
參數綁定:將 Context 對象與回調函數的參數建立關聯
-
處理請求:調用 handleHTTPRequest 方法處理網絡請求
-
回收資源:將處理完成後的 Context 對象放回連接池
下面是 handleHTTPRequest 的關鍵代碼,主要處理 2 點事情。一是根據請求 URL 從基數樹中獲取到對應的處理函數,將其賦值給 Context 對象; 二是調用 Next 方法執行處理函數 handlers,最後將請求返回值寫入 Context 中的 responseWriter 響應對象。
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
}
上下文 (Context)
Context 是 Gin 框架中非常重要的組成部分,HTTP 請求處理時所有的相關數據都存儲在 Context 結構體中。通過前面 Engine 的 ServeHTTP 邏輯可以看到,並不是直接創建 context 對象,而是從 Engine 的對象池中獲取,即 engine.pool 的 Get 方法。獲取到的對象需要先進行重置初始化,使用完畢後通過 Put 放回到 pool 中。
Engine 對象中的 pool 類型是 sync.Pool,它是標準庫中提供的併發安全對象池,主要提供了 Get、Put 和 New 方法。
-
通過 Get() 方法從對象池中獲取對象
-
通過 Put() 方法將使用完的對象歸還到對象池
-
當對象池爲空調用 Get() 方法時,會自動觸發 New 方法創建對象
New() 方法實現如下, 調用 Engine 的 allocateContext 方法創建 Context 對象。
func New() *Engine {
//... Omit other code
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
構建 Context 對象時採用了切片容量預分配策略,避免頻繁的內存申請開銷,下面代碼中的 v 和 skippedNodes 都是提前預分配大小。
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}
}
調用 Context 的 Next 方法才真正執行 handlers 邏輯,代碼實現非常簡潔巧妙, 通過下標訪問處理 c.handlers 中的每個 handler,c.index 初始值爲 - 1,首次 c.index++ 爲 0,執行第一個處理函數。
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
之所以採用下標遍歷 handlers,有一個重要的原因是當某個 handler 執行中觸發 panic,可以通過中間件的 recover 機制捕獲異常後,重新調用 Next() 方法繼續執行後續處理函數,從而確保單個 handler 故障不會中斷整個處理鏈。
異常處理
在 Gin 框架中,某個請求的處理函數產生 panic,程序不會直接崩潰,框架做了異常處理,具體來說就是輸出錯誤信息並保持服務繼續運行,這種處理機制與 Lua 框架中通過 xpcall 執行消息處理函數的容錯策略類似。
這裏我們暫不深究處理細節,只關注核心操作。customRecoveryWithWriter函數返回了一個匿名函數,在匿名函數中通過 defer 註冊了一個匿名函數,主要邏輯有以下 3 點:
-
調用 recover 捕獲 panic
-
執行預定的異常處理邏輯
-
完成上述處理後主動調用 Context 的 Next 方法,確保繼續按照原本的處理函數鏈 handlers 運行
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
}
}
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/se0139z6WbxUCSIoVx2qsg