跨字段与业务规则
标签层覆盖了单字段约束。任何依赖两个或更多字段(确认密码、 条件必填、计算上下界……)的逻辑请使用 Validate() error 钩子。
契约
如果该结构体值(或其指针)实现了:
go
Validate() error…govalid 会在每次 Check 时,于标签规则之后调用它。任何非 nil 的 错误会被作为 *ErrContext 追加到结果切片,并包装你返回的消息。
签名必须精确为 func() error:
| 方法形态 | 是否被调用 |
|---|---|
func (T) Validate() error | 是(值接收者) |
func (*T) Validate() error | 当调用方传入 *T 时 |
func (T) Validate(extra string) error | 否——参数数量错 |
func (T) Validate() string | 否——返回类型错 |
确认密码
go
type Form struct {
Password string `valid:"required;minlen:8" label:"密码"`
RepeatPassword string `valid:"required" label:"重复密码"`
}
func (f *Form) Validate() error {
if f.Password != f.RepeatPassword {
return errors.New("两次输入的密码不一致")
}
return nil
}用内置的 equal 校验器是等价写法:
go
type Form struct {
Password string `valid:"required;minlen:8" label:"密码"`
RepeatPassword string `valid:"required;equal:Password" label:"重复密码"`
}Validate 形态更灵活——可以混合多个条件以及外部查询。
条件必填
go
type Address struct {
Country string `valid:"required;list:CN,US"`
State string // 仅当 Country == "US" 时必填
}
func (a Address) Validate() error {
if a.Country == "US" && a.State == "" {
return errors.New("US 地址必须填写 state")
}
return nil
}外部查询
因为 Validate 就是普通的 Go 代码,所以可以调用数据库、缓存或任何 其他服务。不过别把 Check 当成你的鉴权边界——它没有取消语义,也 不会感知 timeout。
go
type CreateUser struct {
Username string `valid:"required;username"`
Email string `valid:"required;email"`
}
func (u *CreateUser) Validate() error {
if exists, _ := userRepo.UsernameExists(u.Username); exists {
return fmt.Errorf("用户名 %q 已被占用", u.Username)
}
return nil
}TIP
尽量让 Validate 是确定性、无副作用的。校验经常会在一次请求中被 调用多次(预览、保存、重渲染)——意外的副作用会让调试变得很痛苦。
返回更丰富的错误
Validate 可以返回任意 error。govalid 会通过 Error() 在构建 用户消息时字符串化它,所以一个 Error() 返回本地化字符串的自定 义错误类型可以无缝工作:
go
type LocalizedErr struct{ zh, en string }
func (e *LocalizedErr) Error() string { return e.zh }
func (f *Form) Validate() error {
if !ok {
return &LocalizedErr{zh: "操作失败", en: "operation failed"}
}
return nil
}如果你需要拿到原始错误类型,可以对 errs[i].Unwrap() 返回的 error 用 errors.As 类型断言——但要注意 *ErrContext 当前没 有暴露 Unwrap,所以暂时把错误数据放在 Error() 里是最简单的 路径。