Business Rules
The tag layer handles single-field shape; the Validate() method handles everything else. This page collects patterns we've seen in production code.
Cross-field equality
go
type Form struct {
Password string `valid:"required;minlen:8" label:"密码"`
RepeatPassword string `valid:"required;equal:Password" label:"重复密码"`
}Or, equivalently, with Validate:
go
func (f *Form) Validate() error {
if f.Password != f.RepeatPassword {
return errors.New("两次输入的密码不一致")
}
return nil
}Conditional required
go
type Address struct {
Country string `valid:"required;list:CN,US,JP"`
State string
Province string
}
func (a Address) Validate() error {
switch a.Country {
case "US":
if a.State == "" {
return errors.New("US 地址必须填写 state")
}
case "CN":
if a.Province == "" {
return errors.New("中国地址必须填写省份")
}
}
return nil
}Date / time bounds
govalid doesn't ship a date checker. Wrap a custom one:
go
import "time"
govalid.SetMessageTemplates(map[string]string{
"afterToday": "必须晚于今天",
})
govalid.Checkers["afterToday"] = func(c govalid.CheckerContext) *govalid.ErrContext {
t, ok := c.FieldValue.(time.Time)
if !ok {
return govalid.MakeValueTypeError(c)
}
if !t.After(time.Now()) {
return govalid.NewErrorContext(c)
}
return nil
}
type Booking struct {
StartAt time.Time `valid:"required;afterToday" label:"开始时间"`
}Uniqueness against the database
Validate() is plain Go — call your repo:
go
func (u *CreateUser) Validate() error {
if exists, _ := userRepo.UsernameExists(u.Username); exists {
return fmt.Errorf("用户名 %q 已被占用", u.Username)
}
return nil
}WARNING
Validation isn't authorization. If you depend on the database for a security-relevant check, do the check inside the transaction that performs the write — Validate() is a UX nicety, not a guarantee.
Enriching errors
When a single struct can fail in many domain-specific ways, return a typed sentinel from Validate() and switch on it after the fact:
go
var (
errAdminBlocked = errors.New("admin reserved")
errEmailReused = errors.New("email already in use")
)
func (u *CreateUser) Validate() error {
if u.Username == "admin" {
return errAdminBlocked
}
if reused, _ := userRepo.EmailExists(u.Email); reused {
return errEmailReused
}
return nil
}
errs, _ := govalid.Check(form)
for _, e := range errs {
switch {
case strings.Contains(e.Error(), errAdminBlocked.Error()):
// ...
}
}govalid stringifies the error you return, so token-based identification is the simplest path. A future version may expose Unwrap directly.