使用 Go 處理中間件

簡介

開發 web 應用的時候, 很多地方都需要使用中間件來統一處理一些任務,
比如記錄日誌, 登錄校驗等.

gin 也提供了中間件功能.

gin 的中間件

在項目創建之初, 就已經導入了一些中間件, 當時沒有仔細介紹.

g.Use(gin.Logger())
g.Use(gin.Recovery())
g.Use(middleware.NoCache())
g.Use(middleware.Options())
g.Use(middleware.Secure())

前面兩個是 gin 自帶的中間件, 分別是日誌記錄和錯誤恢復.
後面三個是設置一些 header, 具體是阻止緩存響應, 響應 options 請求,
以及瀏覽器安全設置.

// 阻止緩存響應
func NoCache() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        ctx.Header("Cache-Control", "no-cache, no-store, max-age=0, must-revalidate, value")
        ctx.Header("Expires", "Thu, 01 Jan 1970 00:00:00 GMT")
        ctx.Header("Last-Modified", time.Now().UTC().Format(http.TimeFormat))
        ctx.Next()
    }
}

// 響應 options 請求, 並退出
func Options() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        if ctx.Request.Method != "OPTIONS" {
            ctx.Next()
        } else {
            ctx.Header("Access-Control-Allow-Origin", "*")
            ctx.Header("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS")
            ctx.Header("Access-Control-Allow-Headers", "authorization, origin, content-type, accept")
            ctx.Header("Allow", "HEAD,GET,POST,PUT,PATCH,DELETE,OPTIONS")
            ctx.Header("Content-Type", "application/json")
            ctx.AbortWithStatus(200)
        }
    }
}

// 安全設置
func Secure() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        ctx.Header("Access-Control-Allow-Origin", "*")
        ctx.Header("X-Frame-Options", "DENY")
        ctx.Header("X-Content-Type-Options", "nosniff")
        ctx.Header("X-XSS-Protection", "1; mode=block")
        if ctx.Request.TLS != nil {
            ctx.Header("Strict-Transport-Security", "max-age=31536000")
        }

        // Also consider adding Content-Security-Policy headers
        // ctx.Header("Content-Security-Policy", "script-src 'self' https://cdnjs.cloudflare.com")
    }
}

gin 的中間件結構就是一個返回 func(ctx *gin.Context) 的函數,
又叫做 gin.HandlerFunc. 本質上和普通的 handler 沒什麼不同,
gin.HandlerFuncfunc(*Context) 的別名.

中間件可以被定義在三個地方

  • 全局中間件
  • Group 中間件
  • 單個路由中間件

一點需要注意的是在 middleware 和 handler 中使用 goroutine 時,
應該使用 gin.Context 的只讀副本, 例如 cCp := context.Copy().

另一點則是注意中間件的順序.

官方的示例如下:

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()

        // Set example variable
        c.Set("example", "12345")

        // before request

        c.Next()

        // after request
        latency := time.Since(t)
        log.Print(latency)

        // access the status we are sending
        status := c.Writer.Status()
        log.Println(status)
    }
}

創建中間件

介紹了 gin 的中間件知識之後, 就可以根據需求使用中間件了.

實現一箇中間件在每個請求中設置 X-Request-Id 頭.

// 在請求頭中設置 X-Request-Id
func RequestId() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        requestId := ctx.Request.Header.Get("X-Request-Id")

        if requestId == "" {
            requestId = uuid.NewV4().String()
        }

        ctx.Set("X-Request-Id", requestId)

        ctx.Header("X-Request-Id", requestId)
        ctx.Next()
    }
}

設置 header 的同時保存在 context 內部, 通過設置唯一的 ID 之後,
就可以追蹤一系列的請求了.

再來實現一個日誌記錄的中間件, 雖然 gin 已經自帶了日誌記錄的中間件,
但自己實現可以更加個性化.

// 定義日誌組件, 記錄每一個請求
func Logging() gin.HandlerFunc {
    return func(ctx *gin.Context) {
        path := ctx.Request.URL.Path
        method := ctx.Request.Method
        ip := ctx.ClientIP()

        // 只記錄特定的路由
        reg := regexp.MustCompile("(/v1/user|/login)")
        if !reg.MatchString(path) {
            return
        }

        var bodyBytes []byte
        if ctx.Request.Body != nil {
            bodyBytes, _ = ioutil.ReadAll(ctx.Request.Body)
        }
        // 讀取後寫回
        ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

        blw := &bodyLogWriter{
            body:           bytes.NewBufferString(""),
            ResponseWriter: ctx.Writer,
        }
        ctx.Writer = blw

        start := time.Now()
        ctx.Next()
        // 計算延遲, 和 gin.Logger 的差距有點大
        // 這是因爲 middleware 類似棧, 先進後出, ctx.Next() 是轉折點
        // 所以 gin.Logger 放在最前, 記錄總時長
        // Logging 放在最後, 記錄實際運行的時間, 不包含其他中間件的耗時
        end := time.Now()
        latency := end.Sub(start)

        code, message := -1, ""
        var response handler.Response
        if err := json.Unmarshal(blw.body.Bytes(), &response); err != nil {
            logrus.Errorf(
                "response body 不能被解析爲 model.Response struct, body: `%s`, err: `%v`",
                blw.body.Bytes(),
                err,
            )
            code = errno.InternalServerError.Code
            message = err.Error()
        } else {
            code = response.Code
            message = response.Message
        }

        logrus.WithFields(logrus.Fields{
            "latency": fmt.Sprintf("%s", latency),
            "ip":      ip,
            "method":  method,
            "path":    path,
            "code":    code,
            "message": message,
        }).Info("記錄請求")
    }
}

在註冊中間件的時候, 將 Logging 放在全局中間件的最後,
將 gin.Logger() 放在全局中間件的最開始.
通過對比延遲, 你可以發現, 在 handler 處理比較快時,
中間件在總請求耗時中佔據了很大的比例.

所以, 中間件雖然非常實用, 但需要控制全局中間件的數量.

總結

中間件是非常實用的, 基本上 web 框架都會實現.

當前部分的代碼

作爲版本 v0.8.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章