Gin 框架深度分析

Gin 是一個採用 Go 語言實現的 HTTP web 框架,提供了類似 Martini 的 API,但是性能遠強於 Martini,峯值性能是 Martini 的 40 倍。如果我們的項目需要高性能,毫無疑問採用 Gin。

Gin 官網列舉了該項目的 8 個如下關鍵特性:

示例 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 服務非常簡潔,只需三步操作:

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 方法執行下面四個操作:

下面是 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 方法。

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 點:

  1. 調用 recover 捕獲 panic

  2. 執行預定的異常處理邏輯

  3. 完成上述處理後主動調用 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