【问题标题】:Idiomatic way to validate structs验证结构的惯用方法
【发布时间】:2014-05-30 12:47:50
【问题描述】:

我需要验证结构值是否正确,这意味着我需要单独检查每个字段,这对于少数小型结构来说很容易,但我想知道是否有更好的方法来做到这一点。这就是我现在的做法。

type Event struct {
    Id     int
    UserId int
    Start  time.Time
    End    time.Time
    Title  string
    Notes  string
}

func (e Event) IsValid() error {
    if e.Id <= 0 {
        return errors.New("Id must be greater than 0")
    }
    if e.UserId <= 0 {
        return errors.New("UserId must be greater than 0")
    }
    if e.End <= e.Start {
        return errors.New("End must be after Start")
    }
    if e.Start < time.Now() {
        return errors.New("Cannot create events in the past")
    }
    if e.Title == "" {
        return errors.New("Title cannot be empty")
    }
    return nil
}

这是验证结构中字段值的惯用方式吗?看起来很麻烦。

【问题讨论】:

  • 另外看看binding包;它有助于结构验证。结构标签可能有用,但只是在一定程度上。
  • IsXXX 方法通常返回 bool,如果你只想返回一个错误,我可能会将其设为动词(Validate 也许)。非负字段不只是uint 的任何特殊原因?
  • 根据您使用Start, End time.Time 对的频率,您可以创建自己的时间间隔类型(并在那里处理错误的范围)和/或考虑Start time.TimeDuration time.Duration(除非您真的需要两个 time.Location 值的时间)。

标签: validation go


【解决方案1】:

我看不出有任何其他方法可以快速做到这一点。但我找到了一个可以帮助你解决这个问题的 go 包:https://github.com/go-validator/validator

README 文件给出了这个例子:

type NewUserRequest struct {
    Username string `validator:"min=3,max=40,regexp=^[a-zA-Z]$"`
    Name string     `validator:"nonzero"`
    Age int         `validator:"min=21"`
    Password string `validator:"min=8"`
}

nur := NewUserRequest{Username: "something", Age: 20}
if valid, errs := validator.Validate(nur); !valid {
    // values not valid, deal with errors here
}

【讨论】:

  • 我写了这个包。谢谢分享。原始提问者提到的一件事是比较两个字段的能力(e.End &gt; e.Start)。不过,我可以看到这很有用。我想我可能会实现类似的东西。
  • 是的,请!那很好啊。我要试试这个库
  • 看起来像@RobertoSelbach added cross-field validation
【解决方案2】:

为了帮助其他可能正在寻找另一个验证库的人,我创建了以下https://github.com/bluesuncorp/validator

它解决了其他插件尚未实现的一些问题,但该线程中的其他人已经提到过,例如:

  • 返回所有验证错误
  • 每个字段多次验证
  • 跨字段验证前。开始 > 结束日期

受到其他几个项目的启发,包括 go-validator/validator 的公认答案

【讨论】:

  • 看起来很有前途,与 go-validator 相比似乎正在积极开发中! +1
【解决方案3】:

这样做你最终会为每个模型编写大量重复的代码。

使用带有标签的库有其优点和缺点。有时很容易开始,但在路上你会遇到图书馆的限制。

一种可能的方法是创建一个“验证器”,它的唯一职责是跟踪对象内部可能出现的错误。

这个想法的一个非常近似的存根:

http://play.golang.org/p/buBUzk5z6I

package main

import (
    "fmt"
    "time"
)

type Event struct {
    Id     int
    UserId int
    Start  time.Time
    End    time.Time
    Title  string
    Notes  string
}

type Validator struct {
    err error
}

func (v *Validator) MustBeGreaterThan(high, value int) bool {
    if v.err != nil {
        return false
    }
    if value <= high {
        v.err = fmt.Errorf("Must be Greater than %d", high)
        return false
    }
    return true
}

func (v *Validator) MustBeBefore(high, value time.Time) bool {
    if v.err != nil {
        return false
    }
    if value.After(high) {
        v.err = fmt.Errorf("Must be Before than %v", high)
        return false
    }
    return true
}

func (v *Validator) MustBeNotEmpty(value string) bool {
    if v.err != nil {
        return false
    }
    if value == "" {
        v.err = fmt.Errorf("Must not be Empty")
        return false
    }
    return true
}

func (v *Validator) IsValid() bool {
    return v.err != nil
}

func (v *Validator) Error() string {
    return v.err.Error()
}

func main() {
    v := new(Validator)
    e := new(Event)
    v.MustBeGreaterThan(e.Id, 0)
    v.MustBeGreaterThan(e.UserId, 0)
    v.MustBeBefore(e.End, e.Start)
    v.MustBeNotEmpty(e.Title)
    if !v.IsValid() {
        fmt.Println(v)
    } else {
    fmt.Println("Valid")
    }
}
package main

import (
    "fmt"
    "time"
)

type Event struct {
    Id     int
    UserId int
    Start  time.Time
    End    time.Time
    Title  string
    Notes  string
}

type Validator struct {
    err error
}

func (v *Validator) MustBeGreaterThan(high, value int) bool {
    if v.err != nil {
        return false
    }
    if value <= high {
        v.err = fmt.Errorf("Must be Greater than %d", high)
        return false
    }
    return true
}

func (v *Validator) MustBeBefore(high, value time.Time) bool {
    if v.err != nil {
        return false
    }
    if value.After(high) {
        v.err = fmt.Errorf("Must be Before than %v", high)
        return false
    }
    return true
}

func (v *Validator) MustBeNotEmpty(value string) bool {
    if v.err != nil {
        return false
    }
    if value == "" {
        v.err = fmt.Errorf("Must not be Empty")
        return false
    }
    return true
}

func (v *Validator) IsValid() bool {
    return v.err != nil
}

func (v *Validator) Error() string {
    return v.err.Error()
}

func main() {
    v := new(Validator)
    e := new(Event)
    v.MustBeGreaterThan(e.Id, 0)
    v.MustBeGreaterThan(e.UserId, 0)
    v.MustBeBefore(e.End, e.Start)
    v.MustBeNotEmpty(e.Title)
    if !v.IsValid() {
        fmt.Println(v)
    } else {
    fmt.Println("Valid")
    }
}

然后您可以创建您的 Validate 方法并使用相同的代码:

func (e *Event) IsValid() error {
        v := new(Validator)
    v.MustBeGreaterThan(e.Id, 0)
    v.MustBeGreaterThan(e.UserId, 0)
    v.MustBeBefore(e.End, e.Start)
    v.MustBeNotEmpty(e.Title)
    return v.IsValid()
}

【讨论】:

  • 关于库限制,这是您需要处理 any 库的问题,而不仅仅是验证。这就是开源如此酷的原因,您可以改进图书馆并与世界分享。也就是说,我也喜欢你的方法,我认为它可以成为一个有用的包(它最终会受到限制,但我们已经经历过。)
  • 有时确实如此,但是如果库提供了适当的选项,它就不会像您想象的那样有很多限制,例如github.com/bluesuncorp/validator 允许您在提供的那些不能满足您的需求,然后您的函数可以在库中与内置验证一起使用。
【解决方案4】:

我会编写显式代码而不是使用验证库。编写自己的代码的优点是不需要添加额外的依赖项,不需要学习 DSL,并且可以检查依赖于多个字段的结构的属性(例如 start

为了减少样板,我可能会提取一个函数,在不变量为假的情况下,将错误消息添加到错误片段中。

func check(ea *[]string, c bool, errMsg string, ...args) {
    if !c { *ea = append(*ea, fmt.Sprintf(errMsg, ...args)) }
}

func (e *Event) Validate() error {
    var ea []string
    check(&ea, e.ID >= 0, "want positive ID, got %d", e.ID)
    check(&ea, e.Start < e.End, "want start < end, got %s >= %s", e.Start, e.End)
    ...
    if len(ea) > 0 {
        return errors.New(strings.Join(ea, ", "))
    }
    return nil
 }

这会返回结构验证失败的所有方式,而不仅仅是第一种方式,这可能是也可能不是你想要的。

【讨论】:

    【解决方案5】:

    也许你可以试试validating。使用这个库,您可以像这样验证您的结构:

    package main
    
    import (
        "fmt"
        "time"
    
        v "github.com/RussellLuo/validating"
    )
    
    type Event struct {
        Id     int
        UserId int
        Start  time.Time
        End    time.Time
        Title  string
        Notes  string
    }
    
    func (e *Event) Schema() v.Schema {
        return v.Schema{
            v.F("id", &e.Id):          v.Gt(0),
            v.F("user_id", &e.UserId): v.Gt(0),
            v.F("start", &e.Start):    v.Gte(time.Now()),
            v.F("end", &e.End):        v.Gt(e.Start),
            v.F("title", &e.Title):    v.Nonzero(),
            v.F("notes", &e.Notes):    v.Nonzero(),
        }
    }
    
    func main() {
        e := Event{}
        err := v.Validate(e.Schema())
        fmt.Printf("err: %+v\n", err)
    }
    

    【讨论】:

      【解决方案6】:

      另一种不需要反射并在第一个错误时返回的方法是使用 this 之类的东西:

      type Event struct {
          Id     int
          UserId int
          Start  time.Time
          End    time.Time
          Title  string
          Notes  string
      }
      
      func (e *Event) Validate() error {
          return Check(
              Cf(e.Id <= 0, "Expected ID <= 0, got %d.", e.Id),
              Cf(e.Start.UnixNano() > e.End.UnixNano(), "Expected start < end, got %s >= %s.", e.Start, e.End),
          )
      }
      
      type C struct {
          Check bool
          Error error
      }
      
      func Cf(chk bool, errmsg string, params ...interface{}) C {
          return C{chk, fmt.Errorf(errmsg, params...)}
      }
      
      func Check(args ...C) error {
          for _, c := range args {
              if !c.Check {
                  return c.Error
              }
          }
          return nil
      }
      
      func main() {
          a := Event{Id: 1, Start: time.Now()}
          b := Event{Id: -1}
          fmt.Println(a.Validate(), b.Validate())
      }
      

      【讨论】:

      • 这将在每次调用 Validate 时构造所有错误值。你也有倒退的感觉。
      • 好吧,通常你会将错误保存在某处并改用Check(C{e.Id &lt;= 0, errEtc}...)
      【解决方案7】:

      您描述的方法无疑是最直接的方法。

      您可以将反射与struct field tags 一起使用来进行自动验证。但这需要编写一个为您执行此操作的库。好处是,一旦您编写了验证库,您就可以在任何结构中重用它。

      使用此代码的一个示例是:

      type Person struct {
          Name string `minlength:"3" maxlength:"20"`
          Age  int    `min:"18" max:"80"`
      }
      

      您将创建此类型的实例并将其传递到您的验证代码中。 它将使用字段标签中的规则来验证字段值。

      可能有一些库可以为您做这种事情,但我不确定它们的工作情况如何,或者它们是否仍在维护。

      【讨论】:

      • 请记住,使用反射会大大降低代码速度。
      【解决方案8】:

      我认为这是一个更好的方法!

      import (
          "fmt"
      
          "github.com/bytedance/go-tagexpr/validator"
      )
      
      func Example() {
          var vd = validator.New("vd")
      
          type InfoRequest struct {
              Name string `vd:"($!='Alice'||(Age)$==18) && regexp('\\w')"`
              Age  int    `vd:"$>0"`
          }
          info := &InfoRequest{Name: "Alice", Age: 18}
          fmt.Println(vd.Validate(info) == nil)
      }
      

      https://github.com/bytedance/go-tagexpr/tree/master/validator

      【讨论】:

        【解决方案9】:

        我建议您查看Cue Go 集成,它涵盖了许多验证场景,并且特别有趣的是它是映射到各种语言的抽象验证器,从而允许您在系统的各个部分之间共享验证无论语言如何,都避免了基于不同语言的验证器的问题。例如,考虑在 JS 和 Go 代码之间共享您的验证器

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2023-03-09
          • 2010-12-02
          • 1970-01-01
          • 2012-06-18
          • 1970-01-01
          • 2015-04-05
          • 2017-04-23
          • 1970-01-01
          相关资源
          最近更新 更多