Skip to content

跨字段与业务规则

标签层覆盖了单字段约束。任何依赖两个或更多字段(确认密码、 条件必填、计算上下界……)的逻辑请使用 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() 返回的 errorerrors.As 类型断言——但要注意 *ErrContext 当前没 有暴露 Unwrap,所以暂时把错误数据放在 Error() 里是最简单的 路径。

基于 MIT 协议发布。