校验错误翻译与自定义校验器
本章解决什么问题:默认校验错误是英文机读文本,直接回给中文客户端不友好;内置 tag 覆盖不了业务规则(如“结束时间必须晚于开始时间”)时需要自定义。本章分别给出思路与代码骨架。
思路一:注册中文翻译器
go-playground 生态由 locales(语言数据)、universal-translator(翻译器)、validator 的 translations 插件三部分组成。先把 Gin 内部的 validator 实例取出来(binding.Validator.Engine()),再注册中文默认翻译:
var trans ut.Translator
func init() {
v, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
return
}
zh := zhLocales.New()
uni := ut.New(zh, zh)
trans, _ = uni.GetTranslator("zh")
_ = zhTrans.RegisterDefaultTranslations(v, trans)
}
需要 import:gin 的 binding 包、go-playground 的 locales/zh、universal-translator 与 validator v10(含 translations/zh)。之后把校验错误转成中文列表:
func translateErrs(err error) []string {
var ve validator.ValidationErrors
if errors.As(err, &ve) {
msgs := make([]string, 0, len(ve))
for _, fe := range ve {
msgs = append(msgs, fe.Translate(trans))
}
return msgs
}
return []string{err.Error()}
}
翻译默认使用结构体字段名,想让消息显示 json tag 的名字,可对 validator 实例调用 RegisterTagNameFunc 自定义取名字段名的逻辑。
思路二:自定义校验器
用 v.RegisterValidation(tag, fn) 注册自定义规则,fn 的类型固定为 func(fl validator.FieldLevel) bool,签名不符在编译期就会报错。示例:校验“End 晚于 Start”:
v, _ := binding.Validator.Engine().(*validator.Validate)
_ = v.RegisterValidation("later", func(fl validator.FieldLevel) bool {
end, ok := fl.Field().Interface().(time.Time)
if !ok {
return false
}
start, _ := fl.Parent().FieldByName("Start").Interface().(time.Time)
return end.After(start)
})
注册后结构体上直接写 binding:"later" 即可。RegisterValidation 建议放在 init 或包级初始化里执行一次。
两个常见坑
- 自定义 tag 的中文翻译需要额外用 validator 的 RegisterTranslation 注册,RegisterDefaultTranslations 只覆盖内置 tag;
- 需要结构体整体、跨字段的校验时,validator 也提供结构体层级的注册入口(具体 API 以 validator 官方文档为准),也可以退而求其次,把跨字段逻辑放在字段级自定义函数或业务代码里完成。
关键点
- 翻译三件套:locales 语言包 + universal-translator + validator translations 插件,注册到同一个 validator 实例。
- 自定义规则签名是 func(FieldLevel) bool,注册用 RegisterValidation。
- binding.Validator.Engine() 取到的是 Gin 在用的同一个实例,改它即改全局行为。
小结
翻译让错误可读,自定义让规则可扩展,两者都落在 validator 实例上。第 17 章起换一个战场:从文本参数转向文件上传。