validator.New() 实例化校验器最省事,需避免全局共享、正确注册自定义函数、用 dive 校验嵌套结构、用 required_if 替代 omitempty+required 冲突,并将 ValidationErrors 转为可序列化错误。

用 validator 包做结构体字段校验最省事
Go 生态里,go-playground/validator 是事实标准。它不依赖反射以外的运行时机制,性能好、文档全、社区更新勤,比手写 if err != nil 堆砌校验逻辑靠谱得多。
常见错误是直接在 struct tag 里写 required 却忘了注册自定义类型或嵌套校验——这会导致 validate.Struct() 静默跳过字段。
- 必须用
validator.New()实例(不是包级默认实例),否则无法注册自定义函数 - 嵌套结构体要加
validate:"dive",否则只校验顶层字段 -
omitempty和required冲突:空字符串/零值会被跳过,required失效,改用required_if或required_without
type User struct {
Name string `validate:"required,min=2,max=20"`
Email string `validate:"required,email"`
Age int `validate:"gte=0,lte=150"`
}
v := validator.New()
err := v.Struct(&user)
校验失败时返回结构化错误比 panic 更可控
直接 panic(err) 或忽略 err 在服务端极易引发 500 或静默数据污染。应该把 validator.FieldError 转成可序列化的 map 或自定义 error 类型。
注意 err.(validator.ValidationErrors) 类型断言失败很常见——因为 v.Struct() 返回的是 error 接口,底层可能是 nil 或非 validator 错误(比如 JSON 解析失败)。
立即学习“go语言免费学习笔记(深入)”;
- 先用
errors.As(err, &ve)安全提取,别硬断言 - 每个
FieldError的Field()返回字段名(非 tag 名),Tag()才是required这类标识 - 不要拼接原始 error 字符串返回给前端,容易泄露字段名或业务逻辑
var ve validator.ValidationErrors
if errors.As(err, &ve) {
errs := make(map[string]string)
for _, e := range ve {
errs[e.Field()] = e.Error()
}
return errs
}
跨包复用校验逻辑要避免 validator 实例全局共享
很多人图省事把 validator.New() 结果存成包变量,结果在并发请求中注册了冲突的自定义函数,或者被其他模块覆盖了配置(比如设置了 TagName 导致所有 tag 解析错乱)。
典型症状是:A 包注册了 is_phone 校验器,B 包调用 Struct() 时却报 unknown validation;或者某次部署后所有 email 校验突然失效。
- 每个业务模块应持有自己的
*validator.Validate实例 - 若需统一配置(如时间格式、国际化提示),用函数封装初始化逻辑,而非复用实例
- 自定义函数注册必须在首次校验前完成,且不能重复注册(会 panic)
复杂业务规则别硬塞进 validator tag
像「密码和确认密码必须一致」「启用开关为 true 时,URL 字段必填」这类逻辑,写成 eqfield=ConfirmPassword 或 required_if=Enabled true 看似简洁,但很快会失控:tag 变长、难调试、无法打日志、没法单元测试。
validator 的定位是「字段级基础约束」,不是业务规则引擎。一旦出现 or、and 嵌套,或需要查 DB / 调外部 API,就该退出 tag,转到方法内手动校验。
- 把
Validate() error方法加到 struct 上,组合基础校验 + 业务逻辑 - 基础校验失败优先返回,避免后续业务校验无意义执行
- 数据库唯一性校验必须放 service 层,validator 无法感知事务上下文
真正难处理的是校验与领域事件的耦合——比如用户注册成功后才触发邮箱验证。这个时机不在 validator 职责范围内,得靠调用方控制流程。


















