AdSense

網頁

2021/7/31

Golang Gin Router middleware簡介

Go Gin的Middleware(中間件)簡介。


Gin使用router設定路由路徑時,可以透過RouterGroup.Use(middleware ...HandlerFunc)設定middleware。

Middleware的用途為若多個請求路徑需要執行某些相同的邏輯時,可以透過設定middleware來達成。概念類似Java Spring Boot框架的Intercepter是對請求的橫切。

Middleware是一個type為gin.HandlerFunc的函式,簽章為func(*Context)Context為gin的環境變數用來在middleware間傳遞資料。所以middleware翻成「路由中介函式」比較好理解。

範例

範例環境:

  • Go 1.16
  • Gin 1.7.2

目錄結構:

/
├─ main.go
└─ middleware/
   └─ demo_middleware.go

例如下面在middleware/demo_middleware.go中定義一個函式PrintHello()回傳自訂的middleware gin.HandlerFunc()函式實作。收到請求後會先印出"hello-"後接傳入的字串變數s,接著調用Context.Next()呼叫下一個middleware函式,回應時(response)會依相反的順序執行middleware函式中Context.Next()之後的邏輯。

/middleware/demo_middleware.go

package middleware

import (
    "fmt"

    "github.com/gin-gonic/gin"
)

func PrintHello(s string) gin.HandlerFunc {
    return func(c *gin.Context) {

        fmt.Println("hello-" + s)
        // before request

        c.Next() // execute next middleware handler
        // after request

        fmt.Println("bye-" + s)

    }
}

main()中使用RouterGroup.Use()設定middleware PrintHello()

main.go

package main

import (
    "fmt"

    "abc.com/demo/middleware"
    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.Use(
        middleware.PrintHello("1"),
        middleware.PrintHello("2"),
        middleware.PrintHello("3"),
    )

    router.GET("/demo", func(c *gin.Context) {
        msg := "demo"
        fmt.Println(msg)
        c.JSON(200, msg)
    })
    router.Run()
}

測試

使用cURL發送請求給/demo在console則顯示以下。

hello-1
hello-2
hello-3
demo
bye-3
bye-2
bye-1

以圖是來看像是這樣。

            ┌───────────────────────────────┐
            │  PrintHello("1")              │
            │                               │
            │  hello-1                      │
            │  ┌─────────────────────────┐  │
            │  │  PrintHello("2")        │  │
            │  │                         │  │
            │  │  hello-2                │  │
            │  │  ┌───────────────────┐  │  │
            │  │  │  PrintHello("3")  │  │  │
            │  │  │                   │  │  │
GET|/demo   │  │  │  hello-3          │  │  │
────────────┼──┼──┼───────────────────┼──┼──┼───────────┐
            │  │  │                   │  │  │           │ demo
◄───────────┼──┼──┼───────────────────┼──┼──┼───────────┘
200         │  │  │  bye-3            │  │  │
"demo"      │  │  └───────────────────┘  │  │
            │  │  bye-2                  │  │
            │  └─────────────────────────┘  │
            │  bye-1                        │
            └───────────────────────────────┘


原碼分析

Gin middleware handler原始碼運作方式如下。

呼叫RouterGroup.Use()設定middleware handler。

gin.go#L261

// gin.go line 261-266
func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
    engine.RouterGroup.Use(middleware...)
    ...
}

routergroup.go#L51

// routergroup.go line:51-54
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
    group.Handlers = append(group.Handlers, middleware...) // append to engind.RouterGroup.Handlers
    ...
}

在呼叫RouterGroup.GET()設定API path時才會把middleware handler放入Gin的engine.trees中的node

routergroup.go#L102

// routergroup.go line:102-104
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
    return group.handle(http.MethodGet, relativePath, handlers)
}

group.combineHandler()把特定http method的route path的handler參數與既有的route group的middleware handler結合。group.engine.addRoute()handlers放入engine.treesnode

routergroup.go#L72

// routergroup.go line:72-77
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
    ...
    handlers = group.combineHandlers(handlers) // combine parameter handlers with engine.RouterGroup.Handlers 
    group.engine.addRoute(httpMethod, absolutePath, handlers) // set handler in methodTrees's node
    ...
}

以http method從engine.trees取得node並放入handlers

gin.go#L276

// gin.go line:276-295
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
    ...
    root := engine.trees.get(method) // root is a node type with path and handlers
    if root == nil {
        root = new(node)
        root.fullPath = "/"
        engine.trees = append(engine.trees, methodTree{method: method, root: root})
    }
    root.addRoute(path, handlers) // set handlers to tree node
    ...
}

到此Gin的engine中就紀錄了每個route path被呼叫時要執行的handlers(及middleware handlers)。


當Gin接收route的請求時是透過Go的http.ServeHTTP()把收到的RequestResponseWriter轉交給Gin Engine.ServeHTTP()

server.go#L2859

// server.go line:2859-2868
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    handler := sh.srv.Handler
    ...
    handler.ServeHTTP(rw, req) // handler is Gin's Engine
}

然後取得engine建立時的ContextRequestResponseWriter放入後轉交給engine.handleHTTPRequest()

gin.go#L439

// gin.go line:439-448
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)
    ...
}

handleHTTPRequest()中loop engine.trees的每一個node並把其handlers存入Context.handlers然後呼叫Context.Next()執行。

gin.go#L461

// gin.go line:461-519
func (engine *Engine) handleHTTPRequest(c *Context) {
    ...
    // Find root of the tree for the given HTTP method
    t := engine.trees
    for i, tl := 0, len(t); i < tl; i++ {
        ...
        root := t[i].root
        // Find route in tree
        value := root.getValue(rPath, c.params, unescape)
        ...
        if value.handlers != nil {
            c.handlers = value.handlers // set middleware handlers to Context.handlers
            c.fullPath = value.fullPath
            c.Next() // run handlers
            c.writermem.WriteHeaderNow()
            return
        }
        ...
    }

    ...
}

Context.Next()中便會開始loop執行RouterGroup.Use()設定的每一個middleware handlers,並將自身作為參數傳入。

context.go#L62

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c) // invoke handler
        c.index++
    }
}

而middleware handler中調用Context.Next()又會執行上面函示,context的index遞增,從c.handlers取出下一個handler執行。



以上簡化如下。

package main

import "fmt"

func main() {

    handlers := [4]HandlerFunc{
        PrintHello("1"),
        PrintHello("2"),
        PrintHello("3"),
        Demo(),
    }

    c := NewContext()
    c.handlers = handlers[:]
    c.Next()

}

func Demo() HandlerFunc {
    return func(c *Context) {
        fmt.Println("demo")
    }
}

func PrintHello(s string) HandlerFunc {
    return func(c *Context) {
        fmt.Println("hello-" + s)
        c.Next()
        fmt.Println("bye-" + s)
    }
}

func NewContext() Context {
    return Context{
        index: -1,
    }
}

type HandlerFunc func(*Context)

type HandlersChain []HandlerFunc

type Context struct {
    handlers HandlersChain
    index    int8
}

func (c *Context) Next() {
    c.index++
    for c.index < int8(len(c.handlers)) {
        c.handlers[c.index](c)
        c.index++
    }
}


Gin官網文件上只列出基本的範例但都沒有詳細說明,如果得自己去看原始碼學習在使用上的效率會很差。雖然網路上不少人寫相關技術文章但畢竟都不是官方來源。不詳盡的官方文件對於技術的普及是個障礙。豐富的官方文件說明及範例絕對是一個框架是否能被市場採用的關鍵,例如Java Spring Boot或React就是個很好的例子。

啊?還是說自己可以去gin-gonic/website contribute?


沒有留言:

AdSense