使用 Gin 過程中的一些優化

原文: https://hongker.github.io/2020/04/01/golang-gin/

本文介紹 gin 的一些知識點, 如自定義 Response, 中間件等。

gin

Gin 是一個 go 寫的 web 框架,具有高性能的優點。

初級的使用方式不介紹了,具體請查閱官方文檔。官方地址:https://github.com/gin-gonic/gin

以下介紹基於 gin 開發項目的一些常用模塊。

自定義 Response

每個公司都會自定義接口的數據結構。故我們需要基於Json()自定義一個更方便好用的 response

// Response 數據結構體
type Response struct {
    // StatusCode 業務狀態碼
 StatusCode int `json:"status_code"`

    // Message 提示信息
 Message    string      `json:"message"`

    // Data 數據,用interface{}的目的是可以用任意數據
 Data       interface{} `json:"data"`

    // Meta 源數據,存儲如請求ID,分頁等信息
 Meta       Meta        `json:"meta"`

    // Errors 錯誤提示,如 xx字段不能爲空等
 Errors     []ErrorItem `json:"errors"`
}

// Meta 元數據
type Meta struct {
 RequestId      string                 `json:"request_id"`
 // 還可以集成分頁信息等
}


// ErrorItem 錯誤項
type ErrorItem struct {
 Key   string `json:"key"`
 Value string `json:"error"`
}

// New return response instance
func New() *Response {
 return &Response{
  StatusCode: 200,
  Message:    "",
  Data:       nil,
  Meta: Meta{
   RequestId: uuid.NewV4().String(),
  },
  Errors: []ErrorItem{},
 }
}

封裝 gin.Context 以自定義一些方便的方法

// Wrapper include context
type Wrapper struct {
 ctx *gin.Context
}

// WrapContext
func WrapContext(ctx *gin.Context) *Wrapper {
 return &Wrapper{ctx:ctx}
}

// Json 輸出json,支持自定義response結構體
func (wrapper *Wrapper) Json(response *Response) {
 wrapper.ctx.JSON(200, response)
}

// Success 成功的輸出
func (wrapper *Wrapper) Success( data interface{}) {
 response := New()
 response.Data = data
 wrapper.Json(response)
}

// Error 錯誤輸出
func (wrapper *Wrapper) Error( statusCode int, message string) {
 response := New()
 response.StatusCode = statusCode
 response.Message = message
 wrapper.Json(response)
}

使用:

package main

import (
 "github.com/gin-gonic/gin"
 uuid "github.com/satori/go.uuid"
)

func main()  {
 router := gin.Default()
 router.GET("/", func(ctx *gin.Context) {
  WrapContext(ctx).Success("hello,world")
 })

 router.Run(":8088")
}

通過go run main.go運行後,瀏覽器訪問localhost:8088

中間件

介紹一些常用的中間件,如跨域、Jwt 校驗、請求日誌等。

備註

引入中間件比如在註冊路由之前, 謹記!

跨域中間件

package middleware
import (
 "github.com/gin-gonic/gin"
)
// CORS 跨域中間件
func CORS(ctx *gin.Context) {
 method := ctx.Request.Method

 // set response header
 ctx.Header("Access-Control-Allow-Origin", ctx.Request.Header.Get("Origin"))
 ctx.Header("Access-Control-Allow-Credentials""true")
 ctx.Header("Access-Control-Allow-Headers""Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With")
 ctx.Header("Access-Control-Allow-Methods""GET,POST,PUT,PATCH,DELETE,OPTIONS")

    // 默認過濾這兩個請求,使用204(No Content)這個特殊的http status code
 if method == "OPTIONS" || method == "HEAD" { 
  ctx.AbortWithStatus(204)
  return
 }

 ctx.Next()
}

使用如下:

func main() {
    router := gin.Default()
    router.Use(CORS)
    router.GET("/", func(ctx *gin.Context) {
     WrapContext(ctx).Success("hello,world")
    })

    router.Run(":8088")
}

Jwt 校驗

package main

import (
 "errors"
 "github.com/dgrijalva/jwt-go"
 "github.com/gin-gonic/gin"
 "strings"
 "time"
)

var (
 TokenNotExist       = errors.New("token not exist")
 TokenValidateFailed = errors.New("token validate failed")
 ClaimsKey = "uniqueClaimsKey"
 SignKey = "test"
)

// JwtAuth jwt
type JwtAuth struct {
 SignKey []byte
}

// ParseToken parse token
func (jwtAuth JwtAuth) ParseToken(token string) (jwt.Claims, error) {
 tokenClaims, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
  return jwtAuth.SignKey, nil
 })

 if err != nil {
  return nil, err
 }

 if tokenClaims.Claims == nil || !tokenClaims.Valid {
  return nil, TokenValidateFailed
 }

 return tokenClaims.Claims, nil
}

// GenerateToken
func (jwtAuth JwtAuth) GenerateToken(tokenExpireTime int64 /* 過期時間 */, iss string /* key*/) (string, error) {
 now := time.Now().Unix()
 exp := now + tokenExpireTime
 claim := jwt.MapClaims{
  "iss": iss,
  "iat": now,
  "exp": exp,
 }
 token := jwt.NewWithClaims(jwt.SigningMethodHS256, claim)
 tokenStr, err := token.SignedString(jwtAuth.SignKey)
 return tokenStr, err
}

// JWT gin的jwt中間件
func JWT(ctx *gin.Context) {
 // 解析token
 if err := validateToken(ctx); err != nil {
  WrapContext(ctx).Error(401, err.Error())
  ctx.Abort()
  return
 }

 ctx.Next()
}

// validateToken 驗證token
func validateToken(ctx *gin.Context) error {
 // 獲取token
 tokenStr := ctx.GetHeader("Authorization")
 kv := strings.Split(tokenStr, " ")
 if len(kv) != 2 || kv[0] != "Bearer" {
  return TokenNotExist
 }

 jwtAuth := &JwtAuth{SignKey: []byte(SignKey)}
 claims, err := jwtAuth.ParseToken(kv[1])
 if err != nil {
  return err
 }

 // token存入context
 ctx.Set(ClaimsKey, claims)
 return nil
}

使用如下:

func main()  {
 router := gin.Default()
 router.GET("/", func(ctx *gin.Context) {
  WrapContext(ctx).Success("hello,world")
 })

    // 指定user這組路由都需要校驗jwt
 user := router.Group("/user").Use(JWT)
 {
  user.GET("/info", func(ctx *gin.Context) {
   claims, exist := ctx.Get(ClaimsKey)
   if !exist {
    WrapContext(ctx).Error(1001, "獲取用戶信息失敗")
   }
   WrapContext(ctx).Success(claims)
  })
 }


 router.Run(":8088")
}

請求測試:

curl  "localhost:8088/user/info"
// 輸出:
// {"status_code":401,"message":"token not exist","data":null,"meta":{"request_id":"e69361cf-1fd4-42e4-8af8-d18fac1e70fb"},"errors":[]}

// 通過GenerateToken()生成一個token
curl -H "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODU4MjQ2NzgsImlhdCI6MTU4NTgyMTA3OCwiaXNzIjoiYWEifQ.Eyo8KptVUgGfnRG8zsjDilAJOBmaXMtjqJxw__a32HY"  localhost:8088/user/info
// 輸出:
{"status_code":200,"message":"","data":{"exp":1585824678,"iat":1585821078,"iss":"aa"},"meta":{"request_id":"464743de-1033-4656-96f8-36c1529f13e0"},"errors":[]}

請求日誌

記錄每個請求的重要信息

import (
 "bytes"
 "fmt"
 "github.com/gin-gonic/gin"
 "io/ioutil"
 "log"
 "net/http"
 "time"
)

// bodyLogWriter 定義一個存儲響應內容的結構體
type bodyLogWriter struct {
 gin.ResponseWriter
 body *bytes.Buffer
}

// Write 讀取響應數據
func (w bodyLogWriter) Write([]byte) (int, error) {
 w.body.Write(b)
 return w.ResponseWriter.Write(b)
}

// RequestLog gin的請求日誌中間件
func RequestLog(c *gin.Context) {
 // 記錄請求開始時間
 t := time.Now()
 blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
 // 必須!
 c.Writer = blw

 // 獲取請求信息
 requestBody := getRequestBody(c)

 c.Next()

 // 記錄請求所用時間
 latency := time.Since(t)

 // 獲取響應內容
 responseBody := blw.body.String()

 logContext := make(map[string]interface{})
 // 日誌格式
 logContext["request_uri"] = c.Request.RequestURI
 logContext["request_method"] = c.Request.Method
 logContext["refer_service_name"] = c.Request.Referer()
 logContext["refer_request_host"] = c.ClientIP()
 logContext["request_body"] = requestBody
 logContext["request_time"] = t.String()
 logContext["response_body"] = responseBody
 logContext["time_used"] = fmt.Sprintf("%v", latency)
 logContext["header"] = c.Request.Header

 log.Println(logContext)
}

// getRequestBody 獲取請求參數
func getRequestBody(c *gin.Context) interface{} {
 switch c.Request.Method {
 case http.MethodGet:
  return c.Request.URL.Query()

 case http.MethodPost:
  fallthrough
 case http.MethodPut:
  fallthrough
 case http.MethodPatch:
  var bodyBytes []byte // 我們需要的body內容
        // 可以用buffer代替ioutil.ReadAll提高性能
  bodyBytes, err := ioutil.ReadAll(c.Request.Body)
  if err != nil {
   return nil
        }
        // 將數據還回去
  c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

  return string(bodyBytes)

 }

 return nil
}

使用

router.Use(ReqeustLog)

今天就到這兒吧,還有一些比如全局 ID 中間件,後面來寫。

歡迎關注 Go 生態。生態君會不定期分享 Go 語言生態相關內容。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/fTFKKhRdMcJV9j0JOjxEyQ