validator默认不校验嵌套结构体字段,是为避免隐式递归、控制校验深度及防止循环引用崩溃;必须显式添加dive、structonly等标签或调用StructCtx才能触发深度校验。

为什么 validator 默认不校验嵌套结构体字段?
Go 的 validator(如 go-playground/validator)默认只做一层浅校验,struct 字段若没加 validate:"embedded" 或没显式调用 Struct(),内部字段直接被跳过。这不是 bug,是设计取舍:避免隐式递归、控制校验深度、防止循环引用崩溃。
常见错误现象:Address.City 明明写了 validate:"required",但整个 Address 字段为空时校验仍通过;或者 Address 是指针,nil 时子字段校验根本不会触发。
- 必须给嵌套字段加上
validate:"required"(或validate:"required,nonzero")才能确保非空后继续校验子字段 - 若嵌套字段是指针类型(
*Address),需额外加validate:"required"才会进入其内部校验 -
validate:"omitempty"和required同时存在时,前者优先——字段为零值就跳过后续校验,哪怕它本该非空
怎么写一个可复用的嵌套校验函数?
别反复写 err := validate.Struct(req.Address),把逻辑收拢成函数更可控。关键是传入时保留原始错误上下文,且支持多级嵌套中断或继续。
示例函数签名:func ValidateNested(v interface{}, fieldName string) error。它不直接返回 ValidationError,而是包装成带前缀的错误,比如 "address.city: cannot be empty",方便前端解析。
立即学习“go语言免费学习笔记(深入)”;
- 用
validator.New().ValidateStruct(v)单独校验子结构体,避免污染主结构体校验器配置 - 手动拼接字段路径:传入
"address",再从子错误中提取"city",组合成"address.city" - 遇到
nil指针时,立即返回fieldName + ": is required",不调用ValidateStruct - 若子结构体本身含
map或[]struct,需额外循环校验每个元素——这时建议拆成独立校验函数,而非塞进同一层
如何让自定义验证器参与嵌套校验链?
自定义函数(如 isMobile)默认只作用于单个字段,要让它在嵌套结构里生效,关键在注册方式和标签写法。
注册时用 validate.RegisterValidation("mobile", isMobile),然后在嵌套结构体字段上写 validate:"mobile" —— 它会自动被调用,无需额外干预。但注意:自定义函数接收的是字段值本身,不是整个结构体,所以无法访问同级其他字段(比如不能跨字段校验 Password 和 ConfirmPassword)。
- 跨字段校验必须用
StructLevel验证器,注册时用RegisterStructValidation,且只能挂载到顶层结构体上 - 嵌套结构体里的自定义验证器,若依赖外部上下文(如数据库查重),建议改用业务层手动校验,而非塞进
validator链路 - 自定义函数返回错误时,
validator会自动将其转为FieldError并带上字段名,但不会自动加前缀——路径拼接得靠外层函数补全
嵌套校验最容易被忽略的性能与兼容性点
嵌套越深、结构越复杂,校验耗时越不可控。尤其当字段含 map[string]interface{} 或动态 json.RawMessage 时,validator 无法静态分析,可能 panic 或漏校验。
- 对高频接口,禁用
omitempty标签的字段做深度校验——它会让 validator 跳过整块,导致后续逻辑出错 - Go 1.21+ 中
any类型字段若没加具体类型断言,validator会跳过,建议统一用interface{}并配合自定义验证器做类型检查 - 如果嵌套结构体字段是
time.Time,确保 tag 写成validate:"required,datetime=2006-01-02T15:04:05Z",否则默认格式校验失败
真正麻烦的不是写法,是校验边界:哪些该由 validator 做,哪些该交给 service 层判断。比如“用户邮箱是否已注册”,永远不该放在 struct tag 里。


















