业务规则
标签层处理单字段形态;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。