開發 web 應用的時候, 不少地方都須要使用中間件來統一處理一些任務,
好比記錄日誌, 登陸校驗等.git
gin 也提供了中間件功能.github
在項目建立之初, 就已經導入了一些中間件, 當時沒有仔細介紹.web
g.Use(gin.Logger()) g.Use(gin.Recovery()) g.Use(middleware.NoCache()) g.Use(middleware.Options()) g.Use(middleware.Secure())
前面兩個是 gin 自帶的中間件, 分別是日誌記錄和錯誤恢復.
後面三個是設置一些 header, 具體是阻止緩存響應, 響應 options 請求,
以及瀏覽器安全設置.json
// 阻止緩存響應 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.HandlerFunc
是func(*Context)
的別名.瀏覽器
中間件能夠被定義在三個地方緩存
一點須要注意的是在 middleware 和 handler 中使用 goroutine 時,
應該使用 gin.Context 的只讀副本, 例如 cCp := context.Copy()
.安全
另外一點則是注意中間件的順序.app
官方的示例以下:框架
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 的中間件知識以後, 就能夠根據需求使用中間件了.ide
實現一箇中間件在每一個請求中設置 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