Skip to content

业务规则

标签层处理单字段形态;Validate() 方法处理其他一切。本页收集了 我们在生产代码中见过的常见模式。

跨字段相等

go
type Form struct {
    Password       string `valid:"required;minlen:8" label:"密码"`
    RepeatPassword string `valid:"required;equal:Password" label:"重复密码"`
}

或等价地用 Validate

go
func (f *Form) Validate() error {
    if f.Password != f.RepeatPassword {
        return errors.New("两次输入的密码不一致")
    }
    return nil
}

条件必填

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
}

日期 / 时间边界

govalid 没有内置日期校验器。可以包一个自定义的:

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:"开始时间"`
}

数据库唯一性校验

Validate() 是普通的 Go 代码——直接调用 repo:

go
func (u *CreateUser) Validate() error {
    if exists, _ := userRepo.UsernameExists(u.Username); exists {
        return fmt.Errorf("用户名 %q 已被占用", u.Username)
    }
    return nil
}

WARNING

校验不是鉴权。如果你依赖数据库做与安全相关的检查,请把这种检查 放到执行写入的事务内部——Validate() 是 UX 上的友好提示, 而不是保证。

丰富错误信息

当一个结构体可能因为多种领域原因失败时,从 Validate() 返回有 类型的哨兵错误,事后用 switch 区分:

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 会字符串化你返回的错误,所以基于 token 的识别是当前最简单 的路径。未来版本可能会直接暴露 Unwrap

基于 MIT 协议发布。