Cross-field & Business Rules
Tag-based rules cover single-field constraints. For anything that depends on two or more fields (password confirmation, conditional required, computed bounds…) use the Validate() error hook.
The contract
If the struct value (or its pointer) implements:
Validate() error…govalid invokes it after the tag rules run, on every Check call. Any non-nil error is appended to the result slice as an *ErrContext — wrapping the message you returned.
The signature must be exactly func() error:
| Method shape | Called? |
|---|---|
func (T) Validate() error | yes (value receiver) |
func (*T) Validate() error | yes when caller passes *T |
func (T) Validate(extra string) error | no — wrong arity |
func (T) Validate() string | no — wrong return |
Password confirmation
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
}Equivalent with the built-in equal checker:
type Form struct {
Password string `valid:"required;minlen:8" label:"密码"`
RepeatPassword string `valid:"required;equal:Password" label:"重复密码"`
}The Validate form is more flexible — you can mix conditions and external lookups.
Conditional required
type Address struct {
Country string `valid:"required;list:CN,US"`
State string // required only when Country == "US"
}
func (a Address) Validate() error {
if a.Country == "US" && a.State == "" {
return errors.New("state is required for US addresses")
}
return nil
}External lookups
Because Validate is plain Go, you can call out to a database, cache, or any other service. Just don't make Check your authorization boundary — it has no notion of cancellation or timeouts.
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
Keep Validate deterministic and side-effect-free where possible. Validation is often called more than once per request (preview, save, re-render) — surprise side effects make debugging hard.
Returning a richer error
Validate may return any error. govalid stringifies it via Error() when building the user-facing message, so a custom error type whose Error() returns a localized string works seamlessly:
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
}If you need access to the original error type, type-assert through errors.As against the error value returned by errs[i].Unwrap() — note that *ErrContext does not currently expose Unwrap, so for now it's simplest to keep your error data in Error().