title: 定制 Gin 的 Validator 錯誤消息及響應結構
date: 2021-11-14 19:02:00
toc: true
category:
- Golang
- Gin
tags: - Golang
- golang
- Go
- go
- Gin
- gin
- Validator
- validator
- 功能
- 校驗
- 定制
- 實現
- 響應
- 結構
- 錯誤
- 消息
- 本地化
- 翻譯
前言#
在使用 Golang 的 Validator (校驗) 功能時,會發現很多錯誤的提示信息都是英文的。例如
{
"code": -1,
"msg": {
"SingUpForm.Age": "Key: 'SingUpForm.Age' Error:Field validation for 'Age' failed on the 'gte' tag",
"SingUpForm.Email": "Key: 'SingUpForm.Email' Error:Field validation for 'Email' failed on the 'required' tag",
"SingUpForm.Name": "Key: 'SingUpForm.Name' Error:Field validation for 'Name' failed on the 'required' tag",
"SingUpForm.Password": "Key: 'SingUpForm.Password' Error:Field validation for 'Password' failed on the 'required' tag",
"SingUpForm.RePassword": "Key: 'SingUpForm.RePassword' Error:Field validation for 'RePassword' failed on the 'required' tag"
}
}
這樣會造成接口調用者的閱讀困難。
翻閱官方文檔後發現翻譯的工作已經做過了,可以直接將錯誤信息修改為指定語言。
但需要注意的是,目前 go-playground/validator
只支持了 13 種語言,具體參閱官方列出的目錄:validator/translations at master · go-playground/validator
錯誤消息本地化#
直接上代碼,邏輯很清晰。每次出現錯誤信息通過 err.(validator.ValidationErrors)
轉換得到異常對象 validator.ValidationErrors
,調用其 func (ve ValidationErrors) Translate(ut ut.Translator) ValidationErrorsTranslations {}
方法即可。
package main
import (
"fmt"
"net/http"
"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"
)
// 翻譯校驗信息
func main() {
if err := InitTranslate("zh"); err != nil {
fmt.Println("初始化翻譯器出錯")
return
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
var singUpForm SingUpForm
if err := c.ShouldBind(&singUpForm); nil != err {
// 出現錯誤
errors, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"code": -1,
"msg": fmt.Errorf("error getting translation of error message, %s", err.Error()),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"code": -1,
"msg": errors.Translate(translator),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登錄成功",
})
})
// 監聽並在 0.0.0.0:8080 上啟動服務
r.Run(":8080")
}
type SingUpForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
}
// use a single instance , it caches struct info
var (
uni *ut.UniversalTranslator
validate *validator.Validate
translator ut.Translator
)
func InitTranslate(locale string) (err error) {
// 修改 gin 框架中的 validator 引擎屬性,實現翻譯效果
engine, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
fmt.Println("獲取 gin 框架的 validator 出錯")
}
zhT := zh.New()
enT := en.New()
uni := ut.New(enT, zhT, enT)
translator, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(engine, translator)
case "zh":
zh_translations.RegisterDefaultTranslations(engine, translator)
}
return
}
修改後的效果為:
{
"code": -1,
"msg": {
"SingUpForm.Age": "Age必須大於或等於1",
"SingUpForm.Email": "Email為必填字段",
"SingUpForm.Name": "Name為必填字段",
"SingUpForm.Password": "Password為必填字段",
"SingUpForm.RePassword": "RePassword為必填字段"
}
}
響應內容結構屬性名稱首字母小寫#
原來的響應內容:
{
"code": -1,
"msg": {
"SingUpForm.Age": "Age必須大於或等於1",
"SingUpForm.Email": "Email為必填字段",
"SingUpForm.Name": "Name為必填字段",
"SingUpForm.Password": "Password為必填字段",
"SingUpForm.RePassword": "RePassword為必填字段"
}
}
修改後的內容為:
{
"code": -1,
"msg": {
"SingUpForm.age": "age必須大於或等於1",
"SingUpForm.email": "email為必填字段",
"SingUpForm.name": "name為必填字段",
"SingUpForm.password": "password為必填字段",
"SingUpForm.rePassword": "rePassword為必填字段"
}
}
完整代碼為:
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
"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"
)
// 翻譯校驗信息
func main() {
if err := InitTranslate("zh"); err != nil {
fmt.Println("初始化翻譯器出錯")
return
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
var singUpForm SingUpForm
if err := c.ShouldBind(&singUpForm); nil != err {
// 出現錯誤
errors, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"code": -1,
"msg": fmt.Errorf("error getting translation of error message, %s", err.Error()),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"code": -1,
"msg": errors.Translate(translator),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登錄成功",
})
})
// 監聽並在 0.0.0.0:8080 上啟動服務
r.Run(":8080")
}
type SingUpForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
}
// use a single instance , it caches struct info
var (
uni *ut.UniversalTranslator
validate *validator.Validate
translator ut.Translator
)
func InitTranslate(locale string) (err error) {
// 修改 gin 框架中的 validator 引擎屬性,實現翻譯效果
engine, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
fmt.Println("獲取 gin 框架的 validator 出錯")
}
// 實現響應內容 tag 首字母小寫
engine.RegisterTagNameFunc(func(field reflect.StructField) string {
// 如 RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
// 那麼 jsonTag 就是 rePassword
jsonTag := field.Tag.Get("json")
// 分割,例如 strings.SplitN("a,b,c", ",", 2) 那麼就是 [a b,c],strings.SplitN("a,b,c,d", ",", 2) 那麼就是 [a b c,d]
splitN := strings.SplitN(jsonTag, ",", 2)
name := splitN[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New()
enT := en.New()
uni := ut.New(enT, zhT, enT)
translator, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(engine, translator)
case "zh":
zh_translations.RegisterDefaultTranslations(engine, translator)
}
return
}
// 移除響應的 Struct 名稱,例如 `SingUpForm.age` 會變成 `age`
func removeTopStruct(fields map[string]string) map[string]string {
resp := make(map[string]string)
for key, value := range fields {
resp[key[strings.Index(key, ".")+1:]] = value
}
return resp
}
移除響應內容中的構造體#
原來的響應內容:
{
"code": -1,
"msg": {
"SingUpForm.Age": "Age必須大於或等於1",
"SingUpForm.Email": "Email為必填字段",
"SingUpForm.Name": "Name為必填字段",
"SingUpForm.Password": "Password為必填字段",
"SingUpForm.RePassword": "RePassword為必填字段"
}
}
修改後的內容為:
{
"code": -1,
"msg": {
"age": "age必須大於或等於1",
"email": "email為必填字段",
"name": "name為必填字段",
"password": "password為必填字段",
"rePassword": "rePassword為必填字段"
}
}
完整代碼為:
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
"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"
)
// 翻譯校驗信息
func main() {
if err := InitTranslate("zh"); err != nil {
fmt.Println("初始化翻譯器出錯")
return
}
r := gin.Default()
r.POST("/", func(c *gin.Context) {
var singUpForm SingUpForm
if err := c.ShouldBind(&singUpForm); nil != err {
// 出現錯誤
errors, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"code": -1,
"msg": fmt.Errorf("error getting translation of error message, %s", err.Error()),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"code": -1,
"msg": removeTopStruct(errors.Translate(translator)),
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "登錄成功",
})
})
// 監聽並在 0.0.0.0:8080 上啟動服務
r.Run(":8080")
}
type SingUpForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
}
// use a single instance , it caches struct info
var (
uni *ut.UniversalTranslator
validate *validator.Validate
translator ut.Translator
)
func InitTranslate(locale string) (err error) {
// 修改 gin 框架中的 validator 引擎屬性,實現翻譯效果
engine, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
fmt.Println("獲取 gin 框架的 validator 出錯")
}
// 實現響應內容 tag 首字母小寫
engine.RegisterTagNameFunc(func(field reflect.StructField) string {
// 如 RePassword string `json:"rePassword" binding:"required,eqfield=Password"`
// 那麼 jsonTag 就是 rePassword
jsonTag := field.Tag.Get("json")
// 分割,例如 strings.SplitN("a,b,c", ",", 2) 那麼就是 [a b,c],strings.SplitN("a,b,c,d", ",", 2) 那麼就是 [a b c,d]
splitN := strings.SplitN(jsonTag, ",", 2)
name := splitN[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New()
enT := en.New()
uni := ut.New(enT, zhT, enT)
translator, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(engine, translator)
case "zh":
zh_translations.RegisterDefaultTranslations(engine, translator)
}
return
}
// 移除響應的 Struct 名稱,例如 `SingUpForm.age` 會變成 `age`
func removeTopStruct(fields map[string]string) map[string]string {
resp := make(map[string]string)
for key, value := range fields {
resp[key[strings.Index(key, ".")+1:]] = value
}
return resp
}