web-dev-qa-db-fra.com

Manière idiomatique de valider des structures dans Go?

Je dois valider qu'une valeur de structure est correcte et cela signifie que je dois vérifier chaque champ individuellement, ce qui est facile pour un petit nombre de petites structures, mais je me demandais s'il y avait une meilleure façon de le faire. Voici comment je le fais en ce moment.

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
}

Est-ce la manière idiomatique de valider les valeurs des champs dans une structure? Cela semble encombrant.

39
adriaan.wiers

Je ne vois pas d'autre moyen de faire cela rapidement. Mais j'ai trouvé un package qui peut vous aider avec ceci: https://github.com/go-validator/validator

Le fichier README donne cet exemple:

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
}
31
julienc

En procédant ainsi, vous finirez par écrire beaucoup de code en double pour chacun de vos modèles.

L'utilisation d'une bibliothèque avec des balises comporte ses propres avantages et inconvénients. Il est parfois facile de commencer, mais vous rencontrez plus tard les limites de la bibliothèque.

Une approche possible consiste à créer un "Validateur" dont la seule responsabilité est de garder une trace des erreurs possibles dans un objet.

Un bout très approximatif de cette idée:

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

Vous pouvez ensuite créer votre méthode Validate et utiliser le même 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()
}
14
fabrizioM

Pour aider ceux qui recherchent une autre bibliothèque de validation, j'ai créé le fichier suivant: https://github.com/bluesuncorp/validator

Il aborde des problèmes que d'autres plugins n'ont pas encore implémentés et que d'autres dans ce fil ont mentionnés, comme:

  • Renvoyer toutes les erreurs de validation
  • plusieurs validations par champ
  • validation inter-champs ex. Début> Date de fin

Inspiré par plusieurs autres projets, y compris la réponse acceptée de go-validator/validator

9
joeybloggs

J'écrirais du code explicite plutôt que d'utiliser une bibliothèque de validation. L’avantage d’écrire votre propre code est que vous n’ajoutez pas de dépendance supplémentaire, vous n'avez pas besoin d'apprendre une ADSL et vous pouvez vérifier les propriétés de vos structures dépendantes de plusieurs champs ).

Pour réduire le passe-temps, je pourrais extraire une fonction qui ajoute un message d'erreur à une tranche d'erreurs dans le cas où un invariant est faux.

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
 }

Cela retourne tous les moyens par lesquels la structure échoue à la validation plutôt que la première, ce qui peut être ou ne pas être ce que vous voulez.

5
Paul Hankin

Peut-être que vous pouvez essayer valider . Avec cette bibliothèque, vous pouvez valider votre structure comme ceci:

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)
}
3
RussellLuo

Une approche différente qui ne nécessite pas de réflexion et renvoie la première erreur utilise quelque chose comme 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())
}
1
OneOfOne

Je pense que c'est une meilleure façon!

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

0
Henry Lee

La méthode que vous décrivez est certainement la façon la plus simple de le faire.

Vous pouvez utiliser la réflexion avec les balises de champ struct pour effectuer une validation automatisée. Mais cela nécessitera d'écrire une bibliothèque qui le fait pour vous. L'avantage est qu'une fois que vous avez écrit la bibliothèque de validation, vous pouvez la réutiliser avec n'importe quelle structure.

Voici un exemple d'utilisation de ce code:

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

Vous créez une instance de ce type et la transmettez dans votre code de validation . Il utilisera les règles des balises de champ pour valider les valeurs de champ.

Il y a probablement quelques bibliothèques qui font ce genre de choses pour vous, mais je ne sais pas si elles fonctionnent bien ou si elles sont toujours entretenues.

0
jimt