Skip to content

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.

Released under the MIT License.