ShouldBindJSON会清空Body且错误不友好,应分两步:先BindJSON解析再validator.Struct校验,并缓存Body;自定义校验需注册函数、中文翻译器和label标签,错误须用Translate获取友好提示。

ShouldBindJSON 会清空 Body,别直接用它做校验
直接调用 c.ShouldBindJSON(&req) 做参数校验,表面省事,实际埋了两个坑:一是校验失败时返回的错误是原始字段路径(如 Key: 'User.Phone'),前端没法展示「手机号格式不正确」;二是它内部会调用 io.ReadAll(c.Request.Body),读完后 c.Request.Body 就变空了,后续中间件或日志记录再读就是 EOF。
真正安全的做法是分两步:c.BindJSON 只负责解析,不触发校验;再手动调用 validator.Struct 做结构体级校验。这样你能完全控制错误包装逻辑,也能在 Bind 前缓存 Body。
- 必须在任何 Bind 操作前,用
io.ReadAll(c.Request.Body)把原始字节存下来 - 之后用
bytes.NewBuffer和io.NopCloser重建可重复读的Body -
BindJSON不会自动校验,得显式调用validator.Struct(req)
注册自定义正则校验器要配齐三样东西
只写 v.RegisterValidation("phone", ...) 是不够的。Gin 默认用英文提示、字段名裸露、不带业务语义,生产环境根本不能用。关键不是正则怎么写,而是让错误能被翻译、字段能被标注、提示能带上下文。
以下三件事必须按顺序做,缺一不可:
- 调用
v.RegisterValidation("phone", func(fl validator.FieldLevel) bool { ... }),注意用fl.Field().String()取值,别用fl.Param() - 初始化中文翻译器:
uni := ut.New(zhT, zhT); trans, _ := uni.GetTranslator("zh"),再调用zh_trans.RegisterDefaultTranslations(v, trans) - 给结构体字段加
label:"手机号"标签,并在init()里注册 tag 解析函数:v.RegisterTagNameFunc(func(fld reflect.StructField) string { return fld.Tag.Get("label") })
error.Error() 永远拿不到友好提示,必须用 Translate
校验失败后拿到的 err 是 validator.ValidationErrors 类型,但直接调 err.Error() 返回的永远是 Key: 'User.Phone' failed on 'phone' tag 这种格式。它不走翻译流程,也不识别 label 标签。
要拿到「手机号格式不正确」这种提示,必须显式调用:
if errs, ok := err.(validator.ValidationErrors); ok {
c.JSON(400, gin.H{"error": errs.Translate(trans)})
}
这里 trans 必须是你之前初始化好的中文翻译器实例,且 Translate 方法依赖 label 标签和已注册的翻译规则。漏掉任意一个环节,结果都是英文或字段路径。
email 标签没法满足业务需求,必须自定义
内置的 email 校验只检查 RFC 格式,没法处理「必须是 company.com 域名」「邮箱不能是临时邮箱(如 163.com)」这类业务规则。而且它的错误信息固定,无法区分「格式错」和「域名白名单不匹配」。
更麻烦的是:email 是硬编码规则,不接受参数,也不能复用正则表达式。比如你要校验「订单号以 ORD 开头 + 8 位数字」,只能写新标签:
v.RegisterValidation("order_no", func(fl validator.FieldLevel) bool {
return regexp.MustCompile(`^ORD\d{8}$`).MatchString(fl.Field().String())
})
然后在结构体里用 binding:"required,order_no",再配好 label 和翻译——这才是可控、可维护的校验方式。


















