必须调用validator.RegisterValidation("myrule", myFunc)注册自定义规则,且myFunc签名须为func(fl validator.FieldLevel) bool,注册须在首次v.Struct()前完成;required仅判Go零值,非业务“非空”;跨字段校验需用StructLevel而非FieldLevel。

直接用 validator.RegisterValidation 注册函数,别自己 parse tag 字符串——注册晚了、签名错、类型断言漏,自定义规则就静默失效。
为什么 validate:"myrule" 完全没触发
常见现象:结构体字段加了自定义 tag,但 v.Struct() 调用后毫无反应。根本原因不是 validator “不认”,而是它压根没见过这个规则名。
- 必须显式调用
v.RegisterValidation("myrule", myFunc),且myFunc签名严格为func(fl validator.FieldLevel) bool - 注册必须在第一次
v.Struct()之前完成;放在init()最稳,放main()开头也行,但绝不能在 handler 里边校验边注册 -
fl.Field().Interface()返回的是interface{},要取值必须手动类型断言,比如fl.Field().Interface().(string),否则 panic - 返回
false表示校验失败,true表示通过——不是“是否合法”,而是“是否满足条件”
required 对 int/time/[]string 失效?不是 bug,是零值语义
required 只判断 Go 的零值,和业务“非空”不是一回事。误以为它能拦住 0 或空格字符串,是多数人踩坑起点。
-
int零值是0,required永远不报错;业务中0非法?改用gt=0或ne=0 -
time.Time零值是0001-01-01,required不触发;需配合datetime规则或自定义函数判非零 -
[]string零值是[](空切片),required不报错;要求数组非空?加min=1,或用dive做子项校验 -
string零值仅是""," "、"\t"全部放过;需要 trim 后非空?得写自定义规则
跨字段校验(如密码一致性)不能靠 FieldLevel
FieldLevel 函数只能拿到当前字段值,没法访问 Password 字段。硬写 eqfield=Password 是最简方案,但复杂逻辑(比如“若 status=active,则 email 必填”)必须换方式。
立即学习“go语言免费学习笔记(深入)”;
- 改用
v.RegisterStructValidation(myFunc, User{}),函数签名为func(sl validator.StructLevel) - 在函数内用
sl.CurrentStruct.Interface().(*User)拿到结构体指针,再取其他字段值 - 别直接
sl.CurrentStruct.FieldByName("Password")——它返回reflect.Value,得接.Interface()才能比较 -
StructLevel校验在所有FieldLevel之后执行,所以字段级错误(如required)会先报出,不会被跨字段逻辑掩盖
嵌套结构体 + omitempty 的真实行为
很多人以为 omitempty 是“有值才校验”,其实它只控制是否跳过整个子结构体校验,而且只对特定类型生效。
- ✅ 有效类型:
*T、map、slice、chan、func、interface{} - ❌ 无效类型:
T(普通 struct),哪怕所有字段都是零值,也会递归进子字段校验 -
Child *Address `validate:"omitempty"`:当Child == nil时,完全跳过Address校验 -
Child Address `validate:"omitempty"`:这个omitempty实际被忽略,Child即使全零也会进校验
真正难的不是写对那几行 tag,而是理解 validator 只校验你让它看到的字段、只按反射能安全读取的方式工作——字段不可导出、指针未解引用、类型断言漏掉,都会让校验逻辑静默跳过或 panic。


















