校验错误翻译与自定义校验器
一句话结论:Gin 默认用 go-playground/validator 做结构体校验(binding:"required,min=..."),但原始错误是英文、字段名取结构体字段名,直接抛给用户不可读。两条路:① 翻译——用官方 translations/zh 按 tag 注册翻译器,并用 RegisterTagNameFunc 让错误字段名取 json tag;② 自定义校验器——通过 binding.Validator.Engine() 拿到底层 *validator.Validate,RegisterValidation 注册业务规则(手机号、枚举、时间区间)。所有校验失败统一收敛到一个“参数错误”响应里(配合错误码设计),handler 里不散落翻译逻辑。
字段名与翻译
v, _ := binding.Validator.Engine().(*validator.Validate)
// 1. 错误里显示 json tag 而非字段名
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
return fld.Tag.Get("json")
})
// 2. 注册中文翻译(官方翻译包:go-playground/validator/translations/zh)
zhT := zh.New()
trans, _ := ut.New(zhT, zhT).GetTranslator("zh")
for tag, fn := range map[string]func(ut.Translator) error{
"required": zhT.RegisterTranslation, // 示意:每个 tag 都要注册
"min": zhT.RegisterTranslation,
} {
v.RegisterTranslation(tag, trans, registrationFunc(tag), translateFunc(tag))
}
说明:官方 zh 包提供翻译函数,但需要为用到的每个 tag(required/min/max/email…)逐个 RegisterTranslation,没有“注册一次全部生效”的开关。
自定义校验器
var mobileRe = regexp.MustCompile(`^1[3-9]\d{9}$`)
func init() {
v, _ := binding.Validator.Engine().(*validator.Validate)
_ = v.RegisterValidation("mobile", func(fl validator.FieldLevel) bool {
return mobileRe.MatchString(fl.Field().String())
})
}
type Req struct {
Phone string `json:"phone" binding:"required,mobile"`
}
更复杂的跨字段/结构体规则用 RegisterStructValidation 或 validator 的 div/gtfield 等内置关联规则。
统一翻译入口
func BindAndCheck(c *gin.Context, obj any) bool {
if err := c.ShouldBindJSON(obj); err != nil {
var verrs validator.ValidationErrors
if errors.As(err, &verrs) {
// verrs.Translate(trans) 得到可读中文;统一 400 响应
}
return false
}
return true
}
追问记忆点
- 追问:为什么错误显示的是结构体字段名而不是 JSON 字段?——validator 默认用 Field.Name,需要 RegisterTagNameFunc 读 json tag。
- 追问:自定义校验器注册时机?——main/init 阶段一次即可,注册后全局生效;validator 会缓存 struct 规则,别在请求路径里注册。
- 追问:Required 对零值(0/"")的语义?——required 拒绝零值;要允许零值用指针字段 + omitempty 组合设计。
- 记忆点:翻译=zh 包按 tag 注册 + json tag 字段名;扩展=RegisterValidation/RegisterStructValidation;错误统一转 400 参数错误码。