這篇主要講解自定義日志與資料驗證
引數驗證
我們知道,一個請求完全依賴前端的引數驗證是不夠的,需要前后端一起配合,才能萬無一失,下面介紹一下,在Gin框架里面,怎么做介面引數驗證的呢
gin 目前是使用 go-playground/validator 這個框架,截止目前,默認是使用 v10 版本;具體用法可以看看 validator package · go.dev 檔案說明哦
下面以一個單元測驗,簡單說明下如何在tag里驗證前端傳遞過來的資料
簡單的例子
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":"[email protected]"}`,
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
})
}
上面的單元測驗已經把基本的驗證語法都列出來了,剩余的可以根據自身需求查詢檔案進行的配置
日志
使用gin默認的日志
首先來看看,初始化gin的時候,使用了 gin.Deatult() 方法,上一篇文章講過,此時默認使用了2個全域中間件,其中一個就是日志相關的 Logger() 函式,回傳了日志處理的中間件
這個函式是這樣定義的
func Logger() HandlerFunc {
return LoggerWithConfig(LoggerConfig{})
}
繼續跟原始碼,看來真正處理的就是 LoggerWithConfig() 函式了,下面列出部分關鍵原始碼
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
GitHub主頁
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 = https://www.cnblogs.com/im-sean-yang/p/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檔案
zap同樣是比較優秀的日志框架,是由uber公司主導開發的,這里就不單獨舉例子了,可與參考下 zap中間件 的實作
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/33539.html
標籤:Go
上一篇:原始碼解讀 Golang 的 sync.Map 實作原理
下一篇:Golang中的內置函式
