gin使⽤validator库参数校验若⼲实⽤技巧
validator库参数校验若⼲实⽤技巧
本⽂介绍了使⽤validator库做参数校验的⼀些⼗分实⽤的使⽤技巧,包括翻译校验错误提⽰信息、⾃定义提⽰信息的字段名称、⾃定义校验⽅法等。
validator库参数校验若⼲实⽤技巧
在web开发中⼀个不可避免的环节就是对请求参数进⾏校验,通常我们会在代码中定义与请求参数相对应的模型(结构体),借助模型绑定快捷地解析请求中的参数,例如 gin 框架中
的Bind和ShouldBind系列⽅法。本⽂就以 gin 框架的请求参数校验为例,介绍⼀些validator库的实⽤技巧。
gin框架使⽤进⾏参数校验,⽬前已经⽀持github/go-playground/validator/v10了,我们需要在定义结构体时使⽤ binding tag标识相关校验规则,可以查看查看⽀持的所有 tag。
基本⽰例
⾸先来看gin框架内置使⽤validator做参数校验的基本⽰例。
package main
import (
"net/http"
"github/gin-gonic/gin"
)
type SignUpParam struct {
Age        uint8  `json:"age" binding:"gte=1,lte=130"`
Name      string `json:"name" binding:"required"`
Email      string `json:"email" binding:"required,email"`
Password  string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}
func main() {
r := gin.Default()
r.POST("/signup", func(c *gin.Context) {
var u SignUpParam
if err := c.ShouldBind(&u); err != nil {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
/
/ 保存⼊库等业务逻辑代码...
c.JSON(http.StatusOK, "success")
})
_ = r.Run(":8999")
}
我们使⽤curl发送⼀个POST请求测试下:
curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123"}' 127.0.0.1:8999/signup
输出结果:
{"msg":"Key: 'SignUpParam.Email' Error:Field validation for 'Email' failed on the 'email' tag\nKey: 'SignUpParam.Password' Error:Field validation for 'Password' failed on the 'required' tag\nKey: 'SignUpParam.RePasswo 从最终的输出结果可以看到 validator 的检验⽣效了,但是错误提⽰的字段不是特别友好,我们可能需要将它翻译成中⽂。
翻译校验错误提⽰信息
validator库本⾝是⽀持国际化的,借助相应的语⾔包可以实现校验错误提⽰信息的⾃动翻译。下⾯的⽰例代码演⽰了如何将错误提⽰信息翻译成中⽂,翻译成其他语⾔的⽅法类似。
package main
import (
"fmt"
"net/http"
"github/gin-gonic/gin"
"github/gin-gonic/gin/binding"
"github/go-playground/locales/en"
"github/go-playground/locales/zh"
ut "github/go-playground/universal-translator"
"github/go-playground/validator/v10"
enTranslations "github/go-playground/validator/v10/translations/en"
zhTranslations "github/go-playground/validator/v10/translations/zh"
)
// 定义⼀个全局翻译器T
var trans ut.Translator
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现⾃定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
zhT := zh.New() // 中⽂翻译器
enT := en.New() // 英⽂翻译器
// 第⼀个参数是备⽤(fallback)的语⾔环境
// 后⾯的参数是应该⽀持的语⾔环境(⽀持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'validation框架
var ok bool
// 也可以使⽤ uni.FindTranslator(...) 传⼊多个locale进⾏查
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
type SignUpParam struct {
Age        uint8  `json:"age" binding:"gte=1,lte=130"`
Name      string `json:"name" binding:"required"`
Email      string `json:"email" binding:"required,email"`
Password  string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
}
func main() {
if err := InitTrans("zh"); err != nil {
fmt.Printf("init trans failed, err:%v\n", err)
return
}
r := gin.Default()
r.POST("/signup", func(c *gin.Context) {
var u SignUpParam
if err := c.ShouldBind(&u); err != nil {
// 获取validator.ValidationErrors类型的errors
errs, ok := err.(validator.ValidationErrors)
if !ok {
/
/ ⾮validator.ValidationErrors类型错误直接返回
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// validator.ValidationErrors类型错误则进⾏翻译
c.JSON(http.StatusOK, gin.H{
"msg":errs.Translate(trans),
})
return
}
// 保存⼊库等具体业务逻辑代码...
c.JSON(http.StatusOK, "success")
})
_ = r.Run(":8999")
}
同样的请求再来⼀次:
curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123"}' 127.0.0.1:8999/signup
这⼀次的输出结果如下:
{"msg":{"SignUpParam.Email":"Email必须是⼀个有效的邮箱","SignUpParam.Password":"Password为必填字段","SignUpParam.RePassword":"RePassword为必填字段"}}
⾃定义错误提⽰信息的字段名
上⾯的错误提⽰看起来是可以了,但是还是差点意思,⾸先是错误提⽰中的字段并不是请求中使⽤的字段,例如:RePassword是我们后端定义的结构体中的字段名,⽽请求中使⽤的是re_password字段。如何是错误提⽰中的字段使⽤⾃定义的名称,例如jsontag指定的值呢?
只需要在初始化翻译器的时候像下⾯⼀样添加⼀个获取json tag的⾃定义⽅法即可。
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现⾃定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册⼀个获取json tag的⾃定义⽅法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() // 中⽂翻译器
enT := en.New() // 英⽂翻译器
// 第⼀个参数是备⽤(fallback)的语⾔环境
// 后⾯的参数是应该⽀持的语⾔环境(⽀持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
/
/ ... liwenzhou ...
}
再尝试发请求,看⼀下效果:
{"msg":{"ail":"email必须是⼀个有效的邮箱","SignUpParam.password":"password为必填字段","_password":"re_password为必填字段"}}
可以看到现在错误提⽰信息中使⽤的就是我们结构体中jsontag设置的名称了。
但是还是有点瑕疵,那就是最终的错误提⽰信息中⼼还是有我们后端定义的结构体名称——SignUpParam,这个名称其实是不需要随错误提⽰返回给前端的,前端并不需要这个值。我们需要想办法把它去掉。
这⾥参考提供的⽅法,定义⼀个去掉结构体名称前缀的⾃定义⽅法:
func removeTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.Index(field, ".")+1:]] = err
}
return res
}
我们在代码中使⽤上述函数将翻译后的errors做⼀下处理即可:
if err := c.ShouldBind(&u); err != nil {
// 获取validator.ValidationErrors类型的errors
errs, ok := err.(validator.ValidationErrors)
if !ok {
// ⾮validator.ValidationErrors类型错误直接返回
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
// validator.ValidationErrors类型错误则进⾏翻译
// 并使⽤removeTopStruct函数去除字段名中的结构体名称标识
c.JSON(http.StatusOK, gin.H{
"msg": removeTopStruct(errs.Translate(trans)),
})
return
}
看⼀下最终的效果:
{"msg":{"email":"email必须是⼀个有效的邮箱","password":"password为必填字段","re_password":"re_password为必填字段"}}
这⼀次看起来就⽐较符合我们预期的标准了。
⾃定义结构体校验⽅法
上⾯的校验还是有点⼩问题,就是当涉及到⼀些复杂的校验规则,⽐如re_password字段需要与password字段的值相等这样的校验规则,我们的⾃定义错误提⽰字段名称⽅法就不能很好解决错误提⽰信息中的其他字段名称了。
curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123","password":"123","re_password":"321"}' 127.0.0.1:8999/signup
最后输出的错误提⽰信息如下:
{"msg":{"email":"email必须是⼀个有效的邮箱","re_password":"re_password必须等于Password"}}
可以看到re_password字段的提⽰信息中还是出现了Password这个结构体字段名称。这有点⼩⼩的遗憾,毕竟⾃定义字段名称的⽅法不能影响被当成param传⼊的值。
此时如果想要追求更好的提⽰效果,将上⾯的Password字段也改为和json tag⼀致的名称,就需要我们⾃定义结构体校验的⽅法。
例如,我们为SignUpParam⾃定义⼀个校验⽅法如下:
// SignUpParamStructLevelValidation ⾃定义SignUpParam结构体校验函数
func SignUpParamStructLevelValidation(sl validator.StructLevel) {
su := sl.Current().Interface().(SignUpParam)
if su.Password != su.RePassword {
// 输出错误提⽰信息,最后⼀个参数就是传递的param
sl.ReportError(su.RePassword, "re_password", "RePassword", "eqfield", "password")
}
}
然后在初始化校验器的函数中注册该⾃定义校验⽅法即可:
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性,实现⾃定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// ... liwenzhou ...
// 为SignUpParam注册⾃定义校验⽅法
v.RegisterStructValidation(SignUpParamStructLevelValidation, SignUpParam{})
zhT := zh.New() // 中⽂翻译器
enT := en.New() // 英⽂翻译器
/
/ ... liwenzhou ...
}
最终再请求⼀次,看⼀下效果:
{"msg":{"email":"email必须是⼀个有效的邮箱","re_password":"re_password必须等于password"}}
这⼀次re_password字段的错误提⽰信息就符合我们预期了。
⾃定义字段校验⽅法
除了上⾯介绍到的⾃定义结构体校验⽅法,validator还⽀持为某个字段⾃定义校验⽅法,并使⽤RegisterValidation()注册到校验器实例中。
接下来我们来为SignUpParam添加⼀个需要使⽤⾃定义校验⽅法checkDate做参数校验的字段Date。
type SignUpParam struct {
Age        uint8  `json:"age" binding:"gte=1,lte=130"`
Name      string `json:"name" binding:"required"`
Email      string `json:"email" binding:"required,email"`
Password  string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"`
// 需要使⽤⾃定义校验⽅法checkDate做参数校验的字段Date
Date      string `json:"date" binding:"required,datetime=2006-01-02,checkDate"`
}
其中datetime=2006-01-02是内置的⽤于校验⽇期类参数是否满⾜指定格式要求的tag。如果传⼊的date参数不满⾜2006-01-02这种格式就会提⽰如下错误:
{"msg":{"date":"date的格式必须是2006-01-02"}}
针对date字段除了内置的datetime=2006-01-02提供的格式要求外,假设我们还要求该字段的时间必须是⼀个未来的时间(晚于当前时间),像这样针对某个字段的特殊校验需求就需要我们使⽤⾃定义字段校验⽅法了。
⾸先我们要在需要执⾏⾃定义校验的字段后⾯添加⾃定义tag,这⾥使⽤的是checkDate,注意使⽤英⽂分号分隔开。
// customFunc ⾃定义字段级别校验⽅法
func customFunc(fl validator.FieldLevel) bool {
date, err := time.Parse("2006-01-02", fl.Field().String())
if err != nil {
return false
}
if date.Before(time.Now()) {
return false
}
return true
}
定义好了字段及其⾃定义校验⽅法后,就需要将它们联系起来并注册到我们的校验器实例中。
// 在校验器注册⾃定义的校验⽅法
if err := v.RegisterValidation("checkDate", customFunc); err != nil {
return err
}
这样,我们就可以对请求参数中date字段执⾏⾃定义的checkDate进⾏校验了。我们发送如下请求测试⼀下:
curl -H "Content-type: application/json" -X POST -d '{"name":"q1mi","age":18,"email":"123@qq","password":"123", "re_password": "123", "date":"2020-01-02"}' 127.0.0.1:8999/signup 此时得到的响应结果是:
{"msg":{"date":"Key: 'SignUpParam.date' Error:Field validation for 'date' failed on the 'checkDate' tag"}}
这…⾃定义字段级别的校验⽅法的错误提⽰信息很“简单粗暴”,和我们上⾯的中⽂提⽰风格有出⼊,必须想办法搞定它呀!
⾃定义翻译⽅法
我们现在需要为⾃定义字段校验⽅法提供⼀个⾃定义的翻译⽅法,从⽽实现该字段错误提⽰信息的⾃定义显⽰。
// registerTranslator 为⾃定义字段添加翻译功能
func registerTranslator(tag string, msg string) validator.RegisterTranslationsFunc {
return func(trans ut.Translator) error {
if err := trans.Add(tag, msg, false); err != nil {
return err
}
return nil
}
}
// translate ⾃定义字段的翻译⽅法
func translate(trans ut.Translator, fe validator.FieldError) string {
msg, err := trans.T(fe.Tag(), fe.Field())
if err != nil {
panic(fe.(error).Error())
}
return msg
}
定义好了相关翻译⽅法之后,我们在InitTrans函数中通过调⽤RegisterTranslation()⽅法来注册我们⾃定义的翻译⽅法。
// InitTrans 初始化翻译器
func InitTrans(locale string) (err error) {
// ...
// 注册翻译器
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, trans)
}
if err != nil {
return err
}
// 注意!因为这⾥会使⽤到trans实例
// 所以这⼀步注册要放到trans初始化的后⾯
if err := v.RegisterTranslation(
"checkDate",
trans,
registerTranslator("checkDate", "{0}必须要晚于当前⽇期"),
translate,
)
; err != nil {
return err
}
return
}
return
}
这样再次尝试发送请求,就能得到想要的错误提⽰信息了。
{"msg":{"date":"date必须要晚于当前⽇期"}}
总结
本⽂总结的gin框架中validator的使⽤技巧同样也适⽤于直接使⽤validator库,区别仅仅在于我们配置
的是gin框架中的校验器还是由validator.New()创建的校验器。同时使⽤validator库确实能够在⼀定程度上减少我们的编码量,但是它不太可能完美解决我们所有需求,所以你需要到两者之间的平衡点。
参考链接:
validator库参数校验若⼲实⽤技巧

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。