這篇主要講解自定義日誌與數據驗證前端
咱們知道,一個請求徹底依賴前端的參數驗證是不夠的,須要先後端一塊兒配合,才能萬無一失,下面介紹一下,在Gin框架裏面,怎麼作接口參數驗證的呢git
gin 目前是使用 go-playground/validator 這個框架,截止目前,默認是使用 v10
版本;具體用法能夠看看 validator package · go.dev 文檔說明哦github
下面以一個單元測試,簡單說明下如何在tag
裏驗證前端傳遞過來的數據json
func TestValidation(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) testCase := []struct { msg string // 本測試用例的說明 jsonStr string // 輸入的參數 haveErr bool // 是否有 error bindStruct interface{} // 被綁定的結構體 errMsg string // 若是有錯,錯誤信息 }{ { msg: "數據正確: ", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required"` }{}, }, { msg: "數據錯誤: 缺乏required的參數", jsonStr: `{"b":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required"` }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'required' tag", }, { msg: "數據正確: 參數是數字而且範圍 1 <= a <= 10", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,max=10,min=1"` }{}, }, { msg: "數據錯誤: 參數數字不在範圍以內", jsonStr: `{"a":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required,max=10,min=2"` }{}, errMsg: "Key: 'A' Error:Field validation for ‘A’ failed on the ‘min’ tag", }, { msg: "數據正確: 不等於列舉的參數", jsonStr: `{"a":1}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,ne=10"` }{}, }, { msg: "數據錯誤: 不能等於列舉的參數", jsonStr: `{"a":1}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"required,ne=1,ne=2"` // ne 表示不等於 }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'ne' tag", }, { msg: "數據正確: 須要大於10", jsonStr: `{"a":11}`, haveErr: false, bindStruct: &struct { A int `json:"a" binding:"required,gt=10"` }{}, }, // 總結: eq 等於,ne 不等於,gt 大於,gte 大於等於,lt 小於,lte 小於等於 { msg: "參數正確: 長度爲5的字符串", jsonStr: `{"a":"hello"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,len=5"` // 須要參數的字符串長度爲5 }{}, }, { msg: "參數正確: 爲列舉的字符串之一", jsonStr: `{"a":"hello"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,oneof=hello world"` // 須要參數是列舉的其中之一,oneof 也可用於數字 }{}, }, { msg: "參數正確: 參數爲email格式", jsonStr: `{"a":"hello@gmail.com"}`, haveErr: false, bindStruct: &struct { A string `json:"a" binding:"required,email"` }{}, }, { msg: "參數錯誤: 參數不能等於0", jsonStr: `{"a":0}`, haveErr: true, bindStruct: &struct { A int `json:"a" binding:"gt=0|lt=0"` }{}, errMsg: "Key: 'A' Error:Field validation for 'A' failed on the 'gt=0|lt=0' tag", }, // 詳情參考: https://pkg.go.dev/github.com/go-playground/validator/v10?tab=doc } for _, c := range testCase { ctx.Request = httptest.NewRequest("POST", "/", strings.NewReader(c.jsonStr)) if c.haveErr { err := ctx.ShouldBindJSON(c.bindStruct) assert.Error(t, err) assert.Equal(t, c.errMsg, err.Error()) } else { assert.NoError(t, ctx.ShouldBindJSON(c.bindStruct)) } } } // 測試 form 的狀況 // time_format 這個tag 只能在 form tag 下能用 func TestValidationForm(t *testing.T) { ctx, _ := gin.CreateTestContext(httptest.NewRecorder()) testCase := []struct { msg string // 本測試用例的說明 formStr string // 輸入的參數 haveErr bool // 是否有 error bindStruct interface{} // 被綁定的結構體 errMsg string // 若是有錯,錯誤信息 }{ { msg: "數據正確: 時間格式", formStr: `a=2010-01-01`, haveErr: false, bindStruct: &struct { A time.Time `form:"a" binding:"required" time_format:"2006-01-02"` }{}, }, } for _, c := range testCase { ctx.Request = httptest.NewRequest("POST", "/", bytes.NewBufferString(c.formStr)) ctx.Request.Header.Add("Content-Type", binding.MIMEPOSTForm) // 這個很關鍵 if c.haveErr { err := ctx.ShouldBind(c.bindStruct) assert.Error(t, err) assert.Equal(t, c.errMsg, err.Error()) } else { assert.NoError(t, ctx.ShouldBind(c.bindStruct)) } } }
簡單解釋一下,還記得上一篇文章講的單元測試嗎,這裏只須要使用到 gin.Context
對象,因此忽略掉 gin.CreateTestContext()
返回的第二個參數,可是須要將輸入參數
放進 gin.Context
,也就是把 Request
對象設置進去 ,接下來才能使用 Bind
相關的方法哦。後端
其中 binding:
代替框架文檔中的 validate
,由於gin單獨給驗證設置了tag名稱,能夠參考gin源碼 binding/default_validator.go
框架
func (v *defaultValidator) lazyinit() { v.once.Do(func() { v.validate = validator.New() v.validate.SetTagName("binding") // 這裏改成了 binding }) }
上面的單元測試已經把基本的驗證語法都列出來了,剩餘的能夠根據自身需求查詢文檔進行的配置curl
首先來看看,初始化gin的時候,使用了 gin.Deatult()
方法,上一篇文章講過,此時默認使用了2個全局中間件,其中一個就是日誌相關的 Logger()
函數,返回了日誌處理的中間件函數
這個函數是這樣定義的工具
func Logger() HandlerFunc { return LoggerWithConfig(LoggerConfig{}) }
繼續跟源碼,看來真正處理的就是 LoggerWithConfig()
函數了,下面列出部分關鍵源碼post
func LoggerWithConfig(conf LoggerConfig) HandlerFunc { formatter := conf.Formatter if formatter == nil { formatter = defaultLogFormatter } out := conf.Output if out == nil { out = DefaultWriter } notlogged := conf.SkipPaths isTerm := true if w, ok := out.(*os.File); !ok || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd())) { isTerm = false } var skip map[string]struct{} if length := len(notlogged); length > 0 { skip = make(map[string]struct{}, length) for _, path := range notlogged { skip[path] = struct{}{} } } return func(c *Context) { // Start timer start := time.Now() path := c.Request.URL.Path raw := c.Request.URL.RawQuery // Process request c.Next() // Log only when path is not being skipped if _, ok := skip[path]; !ok { // 中間省略這一大塊是在處理打印的邏輯 // …… fmt.Fprint(out, formatter(param)) // 最後是經過 重定向到 out 進行輸出 } } }
稍微解釋下,函數入口傳參是 LoggerConfig
這個定義以下:
type LoggerConfig struct { Formatter LogFormatter Output io.Writer SkipPaths []string }
而調用 Default()
初始化gin時候,這個結構體是一個空結構體,在 LoggerWithConfig
函數中,若是這個結構體內容爲空,會爲它設置一些默認值
默認日誌輸出是到 stdout
的,默認打印格式是由 defaultLogFormatter
這個函數變量控制的,若是想要改變日誌輸出,好比同時輸出到文件
和stdout
,能夠在調用 Default()
以前,設置 DefaultWriter
這個變量;可是若是須要修改日誌格式,則不能調用 Default()
了,能夠調用 New()
初始化gin以後,使用 LoggerWithConfig()
函數,將本身定義的 LoggerConfig
傳入。
默認gin只會打印到 stdout
,咱們若是使用第三方的日誌,則不須要管gin自己的輸出,由於它不會輸出到文件,正常使用第三方的日誌工具便可。因爲第三方的日誌工具,咱們須要實現一下 gin 自己打印接口(好比接口時間,接口名稱,path等等信息)的功能,因此每每須要再定義一箇中間件去打印。
logrus 是一個比較優秀的日誌框架,下面這個例子簡單的使用它來記錄下日誌
func main() { g := gin.Default() gin.DisableConsoleColor() testLogrus(g) if err := g.Run(); err != nil { panic(err) } } func testLogrus(g *gin.Engine) { log := logrus.New() file, err := os.Create("mylog.txt") if err != nil { fmt.Println("err:", err.Error()) os.Exit(0) } log.SetOutput(io.MultiWriter(os.Stdout, file)) logMid := func() gin.HandlerFunc { return func(ctx *gin.Context) { var data string if ctx.Request.Method == http.MethodPost { // 若是是post請求,則讀取body body, err := ctx.GetRawData() // body 只能讀一次,讀出來以後須要重置下 Body if err != nil { log.Fatal(err) } ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // 重置body data = string(body) } start := time.Now() ctx.Next() cost := time.Since(start) log.Infof("方法: %s, URL: %s, CODE: %d, 用時: %dus, body數據: %s", ctx.Request.Method, ctx.Request.URL, ctx.Writer.Status(), cost.Microseconds(), data) } } g.Use(logMid()) // curl 'localhost:8080/send' g.GET("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"msg": "ok"}) }) // curl -XPOST 'localhost:8080/send' -d 'a=1' g.POST("/send", func(ctx *gin.Context) { ctx.JSON(200, gin.H{"a": ctx.PostForm("a")}) }) }
zap文檔
zap一樣是比較優秀的日誌框架,是由uber公司主導開發的,這裏就不單獨舉例子了,可與參考下 zap中間件 的實現