验证结构体的惯用方法

huangapple go评论90阅读模式
英文:

Idiomatic way to validate structs

问题

我需要验证一个结构体的值是否正确,这意味着我需要逐个检查每个字段,对于少量小结构体来说这很容易,但我想知道是否有更好的方法来做这件事。以下是我目前的做法。

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
}

这是验证结构体字段值的惯用方式吗?看起来有些繁琐。

英文:

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 &lt;= 0 {
		return errors.New(&quot;Id must be greater than 0&quot;)
	}
	if e.UserId &lt;= 0 {
		return errors.New(&quot;UserId must be greater than 0&quot;)
	}
	if e.End &lt;= e.Start {
		return errors.New(&quot;End must be after Start&quot;)
	}
	if e.Start &lt; time.Now() {
		return errors.New(&quot;Cannot create events in the past&quot;)
	}
	if e.Title == &quot;&quot; {
		return errors.New(&quot;Title cannot be empty&quot;)
	}
	return nil
}

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

答案1

得分: 50

我找到了一个可以帮助你的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 {
    // 值无效,处理错误信息
}

这个示例展示了如何使用该包进行验证。你可以根据自己的需求修改NewUserRequest结构体,并使用validator.Validate函数来验证结构体的字段值是否有效。如果验证失败,你可以在errs变量中获取错误信息并进行处理。

英文:

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:&quot;min=3,max=40,regexp=^[a-zA-Z]$&quot;`
    Name string     `validator:&quot;nonzero&quot;`
    Age int         `validator:&quot;min=21&quot;`
    Password string `validator:&quot;min=8&quot;`
}

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

答案2

得分: 23

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

使用带有标签的库有其优点和缺点。有时候很容易入手,但在后期可能会遇到库的限制。

一种可能的方法是创建一个“验证器”,它的唯一责任是跟踪对象中可能存在的错误。

这是一个非常粗略的想法的桩代码:

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("必须大于 %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("必须早于 %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("不能为空")
		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("有效")
	}
}

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

然后,你可以创建一个 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()
}
英文:

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 (
&quot;fmt&quot;
&quot;time&quot;
)
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 &lt;= high {
v.err = fmt.Errorf(&quot;Must be Greater than %d&quot;, 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(&quot;Must be Before than %v&quot;, high)
return false
}
return true
}
func (v *Validator) MustBeNotEmpty(value string) bool {
if v.err != nil {
return false
}
if value == &quot;&quot; {
v.err = fmt.Errorf(&quot;Must not be Empty&quot;)
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(&quot;Valid&quot;)
}
}
package main
import (
&quot;fmt&quot;
&quot;time&quot;
)
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 &lt;= high {
v.err = fmt.Errorf(&quot;Must be Greater than %d&quot;, 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(&quot;Must be Before than %v&quot;, high)
return false
}
return true
}
func (v *Validator) MustBeNotEmpty(value string) bool {
if v.err != nil {
return false
}
if value == &quot;&quot; {
v.err = fmt.Errorf(&quot;Must not be Empty&quot;)
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(&quot;Valid&quot;)
}
}

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

答案3

得分: 12

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

它解决了其他插件尚未实现的一些问题,正如本帖中其他人提到的:

  • 返回所有验证错误
  • 每个字段可以进行多个验证
  • 跨字段验证,例如开始日期 > 结束日期

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

英文:

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

答案4

得分: 9

我会为你翻译以下内容:

我更倾向于编写明确的代码,而不是使用验证库。编写自己的代码的优点是不会增加额外的依赖,不需要学习特定领域语言(DSL),并且可以检查与多个字段相关的结构体属性(例如,start < end)。

为了减少样板代码,我可能会提取一个函数,用于在不变式为假的情况下向错误切片中添加错误消息。

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
}

这样返回的是结构体未通过验证的所有方式,而不仅仅是第一个方式,这可能是你想要的,也可能不是。

英文:

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(&amp;ea, e.ID &gt;= 0, &quot;want positive ID, got %d&quot;, e.ID)
check(&amp;ea, e.Start &lt; e.End, &quot;want start &lt; end, got %s &gt;= %s&quot;, e.Start, e.End)
...
if len(ea) &gt; 0 {
return errors.New(strings.Join(ea, &quot;, &quot;))
}
return nil
}

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

答案5

得分: 3

也许你可以尝试使用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)
}

希望对你有帮助!

英文:

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

<!-- language: go -->

package main
import (
&quot;fmt&quot;
&quot;time&quot;
v &quot;github.com/RussellLuo/validating&quot;
)
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(&quot;id&quot;, &amp;e.Id):          v.Gt(0),
v.F(&quot;user_id&quot;, &amp;e.UserId): v.Gt(0),
v.F(&quot;start&quot;, &amp;e.Start):    v.Gte(time.Now()),
v.F(&quot;end&quot;, &amp;e.End):        v.Gt(e.Start),
v.F(&quot;title&quot;, &amp;e.Title):    v.Nonzero(),
v.F(&quot;notes&quot;, &amp;e.Notes):    v.Nonzero(),
}
}
func main() {
e := Event{}
err := v.Validate(e.Schema())
fmt.Printf(&quot;err: %+v\n&quot;, err)
}

答案6

得分: 1

一个不需要反射并在第一个错误时返回的不同方法是使用类似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())
}
英文:

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 &lt;= 0, &quot;Expected ID &lt;= 0, got %d.&quot;, e.Id),
Cf(e.Start.UnixNano() &gt; e.End.UnixNano(), &quot;Expected start &lt; end, got %s &gt;= %s.&quot;, 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())
}

答案7

得分: 1

我建议你查看Cue Go集成,它涵盖了许多验证场景,并且特别有趣的是它是一个抽象的验证器,可以映射到各种语言,从而允许你在系统的各个部分之间共享验证,而不受语言差异验证器的问题的影响。例如,想象一下在JS和Go代码之间共享你的验证器。

另外,一个更具Go特色的解决方案是ozzo-validation

英文:

I would suggest you look up the Cue Go integration, which covers many validation scenarios, and is especially interesting in that it is an abstract validator mapping to various languages, thereby allowing you to share validations among various parts of your system regardless of language, avoiding the issues with diverging language-based validators. Think, for example, of sharing your validators among JS and Go code

Alternatively, a more Go-specific idiomatic solution is ozzo-validation.

答案8

得分: 0

你描述的方法确实是最直接的方法。

你可以使用反射和结构体字段标签来进行自动验证。但这将需要编写一个为你完成此操作的库。好处是,一旦你编写了验证库,你可以在任何结构体中重复使用它。

以下是使用此代码的示例:

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

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

可能有一些已经存在的库可以为你完成这种操作,但我不确定它们的工作效果如何,或者它们是否仍在维护。

英文:

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:&quot;3&quot; maxlength:&quot;20&quot;`
Age  int    `min:&quot;18&quot; max:&quot;80&quot;`
}

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.

答案9

得分: 0

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

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

英文:

I think this is a better way!

import (
	&quot;fmt&quot;

	&quot;github.com/bytedance/go-tagexpr/validator&quot;
)

func Example() {
	var vd = validator.New(&quot;vd&quot;)

	type InfoRequest struct {
		Name string `vd:&quot;($!=&#39;Alice&#39;||(Age)$==18) &amp;&amp; regexp(&#39;\\w&#39;)&quot;`
		Age  int    `vd:&quot;$&gt;0&quot;`
	}
	info := &amp;InfoRequest{Name: &quot;Alice&quot;, Age: 18}
	fmt.Println(vd.Validate(info) == nil)
}

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

huangapple
  • 本文由 发表于 2014年5月30日 20:47:50
  • 转载请务必保留本文链接:https://go.coder-hub.com/23955036.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定