Validation 验证结构的惯用方法

Validation 验证结构的惯用方法,validation,go,Validation,Go,我需要验证一个结构值是否正确,这意味着我需要单独检查每个字段,这对于少数小结构来说很容易,但我想知道是否有更好的方法。我现在就这么做 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

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

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
}
类型事件结构{
Id int
用户ID int
开始时间,时间
结束时间,时间到了
标题字符串
音符串
}
func(e事件)IsValid()错误{

如果e.Id你描述的方法肯定是最直接的方法

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

使用此代码的方法示例如下:

type Person struct {
    Name string `minlength:"3" maxlength:"20"`
    Age  int    `min:"18" max:"80"`
}
您将创建此类型的实例并将其传递到验证代码中。 它将使用字段标记中的规则来验证字段值


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

我看不到其他快速实现这一点的方法。但我找到了一个go软件包,它可以帮助您:

自述文件给出了以下示例:

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
}

我会编写显式代码,而不是使用验证库。编写自己的代码的优点是,不需要添加额外的依赖项,不需要学习DSL,并且可以检查依赖于多个字段(例如,start 为了简化样板,我可能会提取一个函数,在不变量为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
 }
func检查(ea*[]字符串、c布尔、errMsg字符串、…args){
如果!c{*ea=append(*ea,fmt.Sprintf(errMsg,…args))}
}
func(e*事件)Validate()错误{
var ea[]字符串
检查(&ea,e.ID>=0,“想要正ID,获得%d”,e.ID)
检查(&ea,e.开始=%s”,e.开始,e.结束)
...
如果len(ea)>0{
返回errors.New(strings.Join(ea,“,”))
}
归零
}

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

另一种不需要反射并在第一个错误时返回的方法是使用以下方法:

类型事件结构{
Id int
用户ID int
开始时间,时间
结束时间,时间到了
标题字符串
音符串
}
func(e*事件)Validate()错误{
退票(
Cf(e.Id e.End.UnixNano(),“预期开始<结束,获得%s>=%s.”,e.start,e.End),
)
}
C型结构{
检查布尔
误差
}
func Cf(chk bool,errmsg string,params…interface{})C{
返回C{chk,fmt.Errorf(errmsg,params…)
}
函数检查(args…C)错误{
对于u,c:=范围参数{
如果!c.检查{
返回c.错误
}
}
归零
}
func main(){
a:=事件{Id:1,Start:time.Now()}
b:=事件{Id:-1}
fmt.Println(a.Validate(),b.Validate())
}

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

使用带有标签的库有其自身的优点和缺点。有时很容易开始,但在接下来的过程中会遇到库的限制

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

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


为了帮助其他可能正在寻找另一个验证库的人,我创建了以下内容

它解决了一些其他插件尚未实现的问题,而本线程中的其他插件也提到了这些问题,例如:

  • 返回所有验证错误
  • 每个字段有多个验证
  • 跨域验证,例如开始>结束日期
受其他几个项目的启发,包括go validator/validator的公认答案,也许您可以试一试。使用此库,您可以像下面这样验证您的结构:

主程序包
进口(
“fmt”
“时间”
v“github.com/RussellLuo/validating”
)
类型事件结构{
Id int
用户ID int
开始时间,时间
结束时间,时间到了
标题字符串
音符串
}
func(e*事件)模式()与模式{
返回v.模式{
v、 F(“id”和e.id):v.Gt(0),
v、 F(“用户id”和e.UserId):v.Gt(0),
v、 F(“start”和&e.start):v.Gte(time.Now()),
v、 F(“结束”和e.end):v.Gt(e.Start),
v、 F(“标题”和e.title):v.Nonzero(),
v、 F(“注释”和e.notes):v.Nonzero(),
}
}
func main(){
e:=事件{}
错误:=v.Validate(如Schema())
fmt.Printf(“错误:%+v\n”,错误)
}

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

导入(
“fmt”
“github.com/bytedance/go-tagexpr/validator”
)
func示例(){
var vd=新的验证器(“vd”)
类型InfoRequest struct{
名称字符串'vd:($!='Alice'| |(Age)$==18)和®exp('\\w')”`
年龄int`vd:“$>0”`
}
信息:=&InfoRequest{姓名:“爱丽丝”,年龄:18}
fmt.Println(vd.Validate(信息)=nil)
}

请记住,使用反射会大大降低代码的速度。我编写了这个软件包。感谢分享。有一件事是原始询问者提到的,它不能做的,那就是比较两个字段(
e.End>e.Start
).我可以看出这是多么有用。我想我可能会实现类似的东西。是的,请!那太好了。我将尝试像@RobertoSelbach这样的图书馆书籍。关于图书馆的限制,这是你需要处理任何图书馆的事,而不仅仅是验证。这就是为什么开源如此酷,你可以改进l也就是说,我也喜欢你的方法,我认为它可以成为一个有用的软件包(最终会有局限性,但我们一直没有成功)
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())
}
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")
    }
}
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()
}