Gin使用及原始碼簡析

Amos01發表於2023-03-11

1. Gin簡介

前面透過兩篇文章分享了Golang HTTP程式設計的路由分發、請求/響應處理。

可以看出來Golang原生HTTP程式設計在路由分組、動態路由及引數讀取/驗證、構造String/Data/JSON/HTML響應的方法等存在最佳化的空間。

Gin是一個用Golang編寫的高效能Web框架。

  • 基於字首樹的路由,快速且支援動態路由
  • 支援中介軟體及路由分組,將具有同一特性的路由劃入統一組別、設定相同的中介軟體。
    • 比如需要登入的一批介面接入登入許可權認證中介軟體、而不需要登入一批介面則不需要接入
  • ...

 

2. 快速使用

基於gin@v1.8.1,基本使用如下

func main() {
    // Creates a new blank Engine instance without any middleware attached
    engine := gin.New()
    // Global middleware
    // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
    // By default gin.DefaultWriter = os.Stdout
    engine.Use(gin.Logger())
    // Recovery middleware recovers from any panics and writes a 500 if there was one.
    engine.Use(gin.Recovery())
    v1Group := engine.Group("app/v1", accessHandler())
    v1Group.GET("user/info", userInfoLogic())
    engine.Run(":8019")
}

 終端執行go run main.go,輸出如下

$ go run main.go
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /app/v1/user/info         --> main.userInfoLogic.func1 (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8019

透過列印可以看出註冊了GET方法的路由/app/v1/user/info,對應處理函式為main.userInfoLogic

總共包括四個處理函式,按順序為gin.Logger()gin.Recovery()accessHandler()以及userInfoLogic

最終在埠8019啟動了HTTP監聽服務。

 

2.1 建立Engine並使用gin.Logger()gin.Recovery() 兩個全域性中介軟體,對engine下的所有路由都生效

透過程式碼及註釋,gin.Logger()gin.Recovery()放到了Engine.RouterGroup.Handlers切片中。

// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be
// included in the handlers chain for every single request. Even 404, 405, static files...
// For example, this is the right place for a logger or error management middleware.
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    engine.rebuild404Handlers()
    engine.rebuild405Handlers()
    return engine
}
// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...)
    return group.returnObj()
}

 

2.2 建立路由分組v1Group,且該分組使用了accessHandler()accessHandler()v1Group分組路由均生效

// Group creates a new router group. You should add all the routes that have common middlewares or the same path prefix.
// For example, all the routes that use a common middleware for authorization could be grouped.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
    return &RouterGroup{
        Handlers: group.combineHandlers(handlers),
        basePath: group.calculateAbsolutePath(relativePath),
        engine:   group.engine,
    }
}

從程式碼可以看出,返回了新的gin.RouterGroup,並且

v1Group.Handlers = append(group.Handlers, handlers),此時gin.RouterGroup.Handlers[gin.Logger(),gin.Recovery(),accessHandler()]

同時v1Group.basePath = "app/v1"

從程式碼同時可以得出,支援分組巢狀分組。即在v1Group都基礎上在建立分組,比如v1Group.Group("north")

 

2.3 在v1Group下注冊路由user/info,該路由的處理函式是userInfoLogic,方法為GET

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()
}
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)
    ...
}

將分組v1Group的路由字首和當前user/info計算得到完整路由,即app/v1/user/info

合併處理函式,此時handlers = [gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()]

最後將路由及處理函式按http method分組,加入到不同路由樹中。

 

2.4 透過 engine.Run(":8019") 在啟動HTTP服務

// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
func (engine *Engine) Run(addr ...string) (err error) {
    ...
    address := resolveAddress(addr)
    debugPrint("Listening and serving HTTP on %s\n", address)
    err = http.ListenAndServe(address, engine.Handler())
    return
}

這裡呼叫http.ListenAndServe啟動HTTP監聽服務,Engine實現了http.Handler介面,如果有客戶端請求,會呼叫到Engine.ServeHTTP函式。

 

3. 路由過程

// gin.go
func (engine *Engine) handleHTTPRequest(c *Context) {
    httpMethod := c.Request.Method
    rPath := c.Request.URL.Path
    ...
    // Find root of the tree for the given HTTP method
    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)
        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
    }
}
// context.go
func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}

從上面程式碼可以看出,透過http method找到對應的路由樹,再根據URL從路由樹中查詢對應的節點,

獲取到處理函式切片,透過c.Next按透過順序執行處理函式。

對於請求GET /app/v1/user/info,將依次執行[gin.Logger(),gin.Recovery(),accessHandler(),userInfoLogic()] 

 

4. 請求/響應引數處理

func accessHandler() func(*gin.Context) {
    return func(c *gin.Context) {
        // 不允許crul訪問
        if strings.Contains(c.GetHeader("user-agent"), "curl") {
            c.JSON(http.StatusBadRequest, "cant't not visited by curl")
            c.Abort() // 直接退出,避免執行後續處理函式
        }
    }
}
func userInfoLogic() func(*gin.Context) {
    return func(c *gin.Context) {
        id := c.Query("id")
        c.JSON(http.StatusOK, map[string]interface{}{"id": id, "name": "bob", "age": 18})
	}
}

v1Group的通用處理函式accessHandler,達到v1Group下注冊的路由無法用curl訪問的效果。

透過c.Query("id") 獲取URL查詢引數,

透過以下程式碼可以看出,第一次獲取URL查詢時會快取所有URL查詢引數,這減少了記憶體的分配,節省了計算資源。

因為每次呼叫url.ParseQuery都會重新申請快取,重複解析URL。

func (c *Context) Query(key string) (value string) {
    value, _ = c.GetQuery(key)
    return
}
func (c *Context) initQueryCache() {
    if c.queryCache == nil {
        if c.Request != nil {
            c.queryCache = c.Request.URL.Query()
        } else {
            c.queryCache = url.Values{}
        }
    }
}
func (c *Context) GetQueryArray(key string) (values []string, ok bool) {
    c.initQueryCache()
    values, ok = c.queryCache[key]
    return
}

透過c.JSON返回Content-Typeapplication/json的響應體,

這也是Gin對原生net/http程式設計的一個最佳化,對常用的響應型別進行封裝,方便使用者使用。

當然,Gin對請求/響應引數的處理還有其它很多細微的最佳化,這裡就不詳細說明了。

 

5. 總結

Gin使用Map來實現路由匹配,而Gin使用路由樹來實現路由匹配,支援動態路由,記憶體佔用小且路由匹配快。

同時Gin使用快取來最佳化請求引數的處理過程,提供了通用的響應引數處理等,方便使用者使用。

相關文章