使用 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(b []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