drpfu51608120170
2014-05-30 12:47
浏览 59
已采纳

在Go中验证结构的惯用方式?

I need to validate that a struct value is correct and this means I need to check every field individually, which is easy for a small number of small structs but I was wondering if there's a better way to do it. Here's how I'm doing it right now.

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
}

Is this the idiomatic way to validate the values of fields in a struct? It looks cumbersome.

  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

8条回答 默认 最新

  • doudi5524 2014-05-30 13:19
    已采纳

    I don't see any other way to do this quickly. But I found a go package which can help you with this: https://github.com/go-validator/validator

    The README file gives this example:

    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
    }
    
    打赏 评论
  • doujiong2533 2014-05-30 13:19

    The method you describe is certainly the most straight forward way to do it.

    You can use reflection with struct field tags to do automated validation. But this will require writing a library which does this for you. The upside is that once you've written the validation library, you can reuse it with any struct.

    An example of a way to use this code would be:

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

    You would create an instance of this type and pass it into your validation code. It would use the rules in the field tags to validate the field values.

    There are probably a few libraries out there which do this sort of thing for you, but I am not sure how well they work or if they are still being maintained.

    打赏 评论
  • duanmao1932 2014-05-30 16:21

    I'd write explicit code rather than use a validation library. The advantage of writing your own code is that you don't add an extra dependency, you don't need to learn a DSL, and you can check properties of your structs that are dependent on multiple fields (for example, that start < end).

    To cut down on the boilerplate, I might extract a function that adds an error message to a slice of errors in the case an invariant is false.

    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
     }
    

    This returns all ways the struct fails validation rather than just the first, which may or may not be what you want.

    打赏 评论
  • doulan1073 2014-05-30 17:02

    A different approach that doesn't need reflection and returns on the first error is using something like 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())
    }
    
    打赏 评论
  • doubi8965 2014-05-30 17:30

    Doing that way you will end up writing a lot of duplicate code for each of your model.

    Using a library with tags comes with its own pros and cons. Sometimes is easy to start but down the road you hit the library limitations.

    One possible approach is to create a "Validator" that its only responsibility is to keep track of the possible errors inside an object.

    A very approximate stub of this idea:

    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")
        }
    }
    

    You can then create your Validate method and use the same code:

    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()
    }
    
    打赏 评论
  • dongzhi1822 2015-03-16 01:19

    To help anyone else that may be looking for another validation library I created the following https://github.com/bluesuncorp/validator

    It addresses some issues that other plugins have not implemented yet that others in this thread had mentioned such as:

    • Returning all validation errors
    • multiple validations per field
    • cross field validation ex. Start > End date

    Inspired by several other projects including the accepted answer of go-validator/validator

    打赏 评论
  • doudeng9425 2017-08-07 03:02

    Maybe you can give validating a try. With this library, you can validate your struct like this:

    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
    ", err)
    }
    
    打赏 评论
  • dougu1045 2019-01-29 19:40

    I think this is a better way!

    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

    打赏 评论

相关推荐 更多相似问题