一、表單基本校驗
Gin是一個Web框架,提供Web服務,所以很多功能是通過第三方插件集成進去的,這里使用了https://github.com/go-playground/validator來處理的。它實現了結構體值驗證以及基於標簽的單個字段。所以可以將請求體綁定到結構體模型上。
需要在綁定的字段上設置tag,比如,綁定格式為json,需要這樣設置 json:"fieldname" 。
此外,Gin還提供了兩套綁定方法:
1、Must bind
- Methods - Bind, BindJSON, BindXML, BindQuery, BindYAML
- Behavior - 這些方法底層使用 MustBindWith,如果存在綁定錯誤,請求將被以下指令中止 c.AbortWithError(400, err).SetType(ErrorTypeBind),響應狀態代碼會被設置為400,請求頭Content-Type被設置為text/plain; charset=utf-8。注意,如果你試圖在此之后設置響應代碼,將會發出一個警告 [GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422,如果你希望更好地控制行為,請使用ShouldBind相關的方法
2、Should bind
- Methods - ShouldBind, ShouldBindJSON, ShouldBindXML, ShouldBindQuery, ShouldBindYAML
- Behavior - 這些方法底層使用 ShouldBindWith,如果存在綁定錯誤,則返回錯誤,開發人員可以正確處理請求和錯誤。
當我們使用綁定方法時,Gin會根據Content-Type推斷出使用哪種綁定器,如果你確定你綁定的是什么,你可以使用MustBindWith或者BindingWith。
你還可以給字段指定特定規則的修飾符,如果一個字段用binding:"required"修飾,並且在綁定時該字段的值為空,那么將返回一個錯誤。
package main import ( "github.com/gin-gonic/gin" "net/http" ) type LoginForm struct { UserName string `form:"username" json:"username" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { var loginForm LoginForm if err := c.ShouldBind(&loginForm); err != nil { c.JSON(http.StatusBadRequest, gin.H{ "error": err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "msg": "登錄成功", }) }) router.Run(":8000") }
然后可以通過客戶端發送請求:
import requests res = requests.post(url="http://127.0.0.1:8000/login", data={ "username": "bily", "password": "123456" }) print(res.text)
但是假如輸入的請求體不符合后台驗證規則,會出現下面的錯誤:
{"error":"Key: 'LoginForm.Password' Error:Field validation for 'Password' failed on the 'required' tag"}
那么怎么把上述的英文轉成中文呢?
二、表單驗證錯誤處理
可以看到上面的錯誤提示都是一些后端定義的字段名以及英文提示,對於用戶來說不是很友好,所以需要將其轉成中文提示。
package main import ( "fmt" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/locales/en" "github.com/go-playground/locales/zh" ut "github.com/go-playground/universal-translator" "github.com/go-playground/validator/v10" en_translations "github.com/go-playground/validator/v10/translations/en" zh_translations "github.com/go-playground/validator/v10/translations/zh" "net/http" "reflect" "strings" ) var trans ut.Translator // 初始化一個翻譯器函數 func InitTrans(locale string) (err error) { // 修改gin框架中的validator引擎屬性,實現定制 v, ok := binding.Validator.Engine().(*validator.Validate) if ok { // 注冊一個獲取json的tag自定義方法 v.RegisterTagNameFunc(func(field reflect.StructField) string { name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0] if name == "-" { return "" } return name }) } zhT := zh.New() //中文翻譯器 enT := en.New() //英文翻譯器 uni := ut.New(enT, zhT, enT) // 配置默認翻譯器、以及可支持的翻譯器 trans, ok = uni.GetTranslator(locale) if !ok { return fmt.Errorf("uni.GetTranslator(%s) error", locale) } switch locale { case "en": _ = en_translations.RegisterDefaultTranslations(v, trans) case "zh": _ = zh_translations.RegisterDefaultTranslations(v, trans) default: _ = en_translations.RegisterDefaultTranslations(v, trans) } return } type LoginForm struct { UserName string `form:"username" json:"username" binding:"required"` Password string `form:"password" json:"password" binding:"required"` } func main() { router := gin.Default() router.POST("/login", func(c *gin.Context) { // 調用翻譯器函數 if err := InitTrans("zh"); err != nil { fmt.Println("翻譯器錯誤!") return } var loginForm LoginForm if err := c.ShouldBind(&loginForm); err != nil { if errors, ok := err.(validator.ValidationErrors); !ok { // 如果錯誤不能轉化,不能進行翻譯,就返回錯誤信息 c.JSON(http.StatusOK, gin.H{ "error": errors.Error(), }) } else { // 如果錯誤能進行翻譯,就返回翻譯后的錯誤信息 c.JSON(http.StatusBadRequest, gin.H{ "error": errors.Translate(trans), }) } return } c.JSON(http.StatusOK, gin.H{ "msg": "登錄成功", }) }) router.Run(":8000") }
假如進行一個錯誤請求:
import requests res = requests.post(url="http://127.0.0.1:8000/login", data={ "password": "123456" }) print(res.text)
如下為錯誤信息:
{"error":{"LoginForm.username":"username為必填字段"}}
着只解決了部分問題,但是字段沒有翻譯過來,所以還需要進行優化:
... func removeTopStruct(fields map[string]string) map[string]string { rsp := map[string]string{} for key, value := range fields { rsp[key[strings.Index(key, ".")+1:]] = value } return rsp } ...
在之前的錯誤處理main函數中引入該函數:
... else { // 如果錯誤能進行翻譯,就返回翻譯后的錯誤信息 c.JSON(http.StatusBadRequest, gin.H{ "error": removeTopStruct(errors.Translate(trans)), }) } ...
錯誤處理的效果為:
{"error":{"username":"username為必填字段"}}
