go参数验证器ozzo-validation的使用

澹台博文
2023-12-01

验证器

描述

ozzo-validation是一个Go软件包,提供可配置和可扩展的数据验证功能。它具有以下功能:

  • 使用常规的编程构造而不是容易出错的构造标记来指定应如何验证数据。
  • 可以验证不同类型的数据,例如结构,字符串,字节片,片,映射,数组。
  • 只要实现Validatable接口,就可以验证自定义数据类型。
  • 可以验证实现该sql.Valuer接口的数据类型(例如sql.NullString)。
  • 可自定义且格式正确的验证错误。
  • 立即提供丰富的验证规则集。
  • 创建和使用自定义验证规则非常容易。
  • github地址: https://github.com/go-ozzo/ozzo-validation

要求

达到1.13或更高。

入门

ozzo验证包主要包括一组验证规则和两种验证方法。您使用验证规则来描述应如何将值视为有效,然后调用validation.Validate() 或validation.ValidateStruct()验证值。

安装

运行以下命令以安装软件包:

	go get github.com/go-ozzo/ozzo-validation/v3
	go get github.com/go-ozzo/ozzo-validation/v3/is

验证简单值

对于简单的值(例如字符串或整数),可以使用validation.Validate()它进行验证。例如,

package main

import (
	"fmt"

	"github.com/go-ozzo/ozzo-validation/v3"
	"github.com/go-ozzo/ozzo-validation/v3/is"
)

func main() {
	data := "example"
	err := validation.Validate(data,
		validation.Required,       // not empty
		validation.Length(5, 100), // length between 5 and 100
		is.URL,                    // is a valid URL
	)
	fmt.Println(err)
	// Output:
	// must be a valid URL
}

该方法validation.Validate()将按照列出的顺序运行规则。如果规则未能通过验证,则该方法将返回相应的错误,并跳过其余规则。如果值通过所有验证规则,则该方法将返回nil。

验证结构

对于结构值,通常需要检查其字段是否有效。例如,在RESTful应用程序中,您可以将请求有效内容解组为结构,然后验证结构字段。如果一个或多个字段无效,则可能需要获取描述哪些字段无效的错误。您可以使用validation.ValidateStruct() 以实现此目的。单个结构可以具有多个字段的规则,并且一个字段可以与多个规则关联。例如,

package main

import (
	"fmt"
	"regexp"

	"github.com/go-ozzo/ozzo-validation/v3"
	"github.com/go-ozzo/ozzo-validation/v3/is"
)

type Address struct {
	Street string
	City   string
	State  string
	Zip    string
}

func (a Address) Validate() error {
	return validation.ValidateStruct(&a,
		// Street cannot be empty, and the length must between 5 and 50
		validation.Field(&a.Street, validation.Required, validation.Length(5, 50)),
		// City cannot be empty, and the length must between 5 and 50
		validation.Field(&a.City, validation.Required, validation.Length(5, 50)),
		// State cannot be empty, and must be a string consisting of two letters in upper case
		validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
		// State cannot be empty, and must be a string consisting of five digits
		validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
	)
}

func main() {
	a := Address{
		Street: "123",
		City:   "Unknown",
		State:  "Virginia",
		Zip:    "12345",
	}

	err := a.Validate()
	fmt.Println(err)
	// Output:
	// Street: the length must be between 5 and 50; State: must be in a valid format.
}

请注意,在调用validation.ValidateStruct以验证结构时,应将指向结构的指针(而不是结构本身)传递给方法。同样,在调用validation.Field以指定结构字段的规则时,应使用指向结构字段的指针。

执行结构验证时,将按照在中指定的顺序验证字段ValidateStruct。并且,当每个字段都经过验证时,其规则也会按照与该字段关联的顺序进行评估。如果规则失败,则会为该字段记录一个错误,并且验证将在下一个字段继续。

验证错误

该validation.ValidateStruct方法返回在结构字段中发现的验证错误,这些验证错误validation.Errors 是字段及其对应错误的映射。如果验证通过,则返回Nil。

默认情况下,validation.Errors使用命名的struct标记json来确定应使用哪些名称来表示无效字段。该类型还实现了json.Marshaler接口,以便可以将其编组为适当的JSON对象。例如,

type Address struct {
	Street string `json:"street"`
	City   string `json:"city"`
	State  string `json:"state"`
	Zip    string `json:"zip"`
}

// ...perform validation here...

err := a.Validate()
b, _ := json.Marshal(err)
fmt.Println(string(b))
// Output:
// {"street":"the length must be between 5 and 50","state":"must be in a valid format"}

您可以修改validation.ErrorTag以使用其他结构标记名称。

如果您不喜欢ValidateStruct根据结构字段名称或相应的标记值确定错误键的魔术,则可以使用以下替代方法:

c := Customer{
	Name:  "Qiang Xue",
	Email: "q",
	Address: Address{
		State:  "Virginia",
	},
}

err := validation.Errors{
	"name": validation.Validate(c.Name, validation.Required, validation.Length(5, 20)),
	"email": validation.Validate(c.Name, validation.Required, is.Email),
	"zip": validation.Validate(c.Address.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))),
}.Filter()
fmt.Println(err)
// Output:
// email: must be a valid email address; zip: cannot be blank.

在上面的示例中,我们validation.Errors通过名称列表和相应的验证结果来构建。最后,我们要求Errors.Filter()从Errors所有与成功验证结果相对应的nil中删除。如果Errors为空,该方法将返回nil 。

上面的方法非常灵活,因为它允许您自由地建立验证错误结构。您可以使用它来验证结构和非结构值。与用于ValidateStruct验证结构相比,它的缺点是您必须冗余地指定错误密钥,同时ValidateStruct可以自动找出它们。

内部错误

内部错误与验证错误的不同之处在于,内部错误是由代码故障(例如,验证器在远程服务中断时进行远程调用以验证某些数据)引起的,而不是由正在验证的数据引起的。在数据验证期间发生内部错误时,您可以允许用户重新提交相同的数据以再次执行验证,以希望程序恢复运行。另一方面,如果由于数据错误导致数据验证失败,则用户通常不应再次重新提交相同的数据。

为了将内部错误与验证错误区分开,当验证程序中发生内部错误时,请validation.InternalError通过调用将其包装validation.NewInternalError()。然后,验证者的用户可以检查返回的错误是否是内部错误。例如,

if err := a.Validate(); err != nil {
	if e, ok := err.(validation.InternalError); ok {
		// an internal error happened
		fmt.Println(e.InternalError())
	}
}

可验证的类型

如果类型实现了validation.Validatable接口,则该类型是有效的。

当validation.Validate用于验证有效值时,如果在给定的验证规则中未发现任何错误,它将进一步调用该值的Validate()方法。

同样,在validation.ValidateStruct验证类型可验证的结构字段时,它将Validate在通过列出的规则后调用该字段的方法。

注意:在实现时validation.Validatable,请勿调用validation.Validate()验证其原始类型的值,因为这将导致无限循环。例如,如果你定义一个新类型MyString作为string 和实施validation.Validatable为MyString,在内部Validate()功能,你应该值转换为string调用之前先validation.Validate()对其进行验证。

在以下示例中,由于实现 ,的Address字段Customer是有效的。因此,在使用验证结构时,验证将“潜入”该领域。Addressvalidation.ValidatableCustomervalidation.ValidateStructAddress

type Customer struct {
	Name    string
	Gender  string
	Email   string
	Address Address
}

func (c Customer) Validate() error {
	return validation.ValidateStruct(&c,
		// Name cannot be empty, and the length must be between 5 and 20.
		validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
		// Gender is optional, and should be either "Female" or "Male".
		validation.Field(&c.Gender, validation.In("Female", "Male")),
		// Email cannot be empty and should be in a valid email format.
		validation.Field(&c.Email, validation.Required, is.Email),
		// Validate Address using its own validation rules
		validation.Field(&c.Address),
	)
}

c := Customer{
	Name:  "Qiang Xue",
	Email: "q",
	Address: Address{
		Street: "123 Main Street",
		City:   "Unknown",
		State:  "Virginia",
		Zip:    "12345",
	},
}

err := c.Validate()
fmt.Println(err)
// Output:
// Address: (State: must be in a valid format.); Email: must be a valid email address.

有时,您可能要跳过对类型Validate方法的调用。为此,只需将validation.Skip规则与要验证的值相关联即可。

Maps/Slices/Arrays 可验证

验证其元素类型实现validation.Validatable接口的可迭代(映射,切片或数组)时,该validation.Validate方法将调用Validate每个非null元素的方法。将返回元素的验证错误,因为validation.Errors它将无效元素的键映射到其相应的验证错误。例如,

addresses := []Address{
	Address{State: "MD", Zip: "12345"},
	Address{Street: "123 Main St", City: "Vienna", State: "VA", Zip: "12345"},
	Address{City: "Unknown", State: "NC", Zip: "123"},
}
err := validation.Validate(addresses)
fmt.Println(err)
// Output:
// 0: (City: cannot be blank; Street: cannot be blank.); 2: (Street: cannot be blank; Zip: must be in a valid format.).

当validation.ValidateStruct用于验证结构时,以上验证过程也适用于那些结构字段,这些结构字段是可验证对象的映射/切片/数组。

Each

另一种选择是使用该validation.Each方法。此方法允许您为结构中的可迭代对象定义规则。

type Customer struct {
    Name      string
    Emails    []string
}

func (c Customer) Validate() error {
    return validation.ValidateStruct(&c,
        // Name cannot be empty, and the length must be between 5 and 20.
		validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
		// Emails are optional, but if given must be valid.
		validation.Field(&c.Emails, validation.Each(is.Email)),
    )
}

c := Customer{
    Name:   "Qiang Xue",
    Emails: []Email{
        "valid@example.com",
        "invalid",
    },
}

err := c.Validate()
fmt.Println(err)
// Output:
// Emails: (1: must be a valid email address.).

指针

当要验证的值是指针时,大多数验证规则将验证指针指向的实际值。如果指针为零,则这些规则将跳过验证。

validation.Required和validation.NotNil规则是一个例外。当指针为零时,它们将报告验证错误。

类型实施 sql.Valuer

如果数据类型实现了sql.Valuer接口(例如sql.NullString),则内置的验证规则将正确处理该接口。特别是,当规则验证此类数据时,它将调用该Value()方法并验证返回的值。

必需vs.非零

验证输入值时,有两种有关检查是否提供输入值的方案。

在第一种情况下,如果未输入输入值或将其输入为零值(例如,空字符串,零整数),则认为输入值丢失。validation.Required在这种情况下,您可以使用规则。

在第二种情况下,仅当未输入输入值时,才将其视为丢失。通常在这种情况下使用指针字段,以便您可以通过检查指针是否为nil来检测是否输入了值。您可以使用validation.NotNil规则来确保输入值(即使它是零值)。

嵌入式结构

该validation.ValidateStruct方法将正确验证包含嵌入式结构的结构。特别是,将嵌入式结构的字段视为直接属于包含的结构。例如,

type Employee struct {
	Name string
}

func ()

type Manager struct {
	Employee
	Level int
}

m := Manager{}
err := validation.ValidateStruct(&m,
	validation.Field(&m.Name, validation.Required),
	validation.Field(&m.Level, validation.Required),
)
fmt.Println(err)
// Output:
// Level: cannot be blank; Name: cannot be blank.

在上面的代码中,我们用于&m.Name指定Name嵌入式struct 的字段的验证Employee。验证错误Name用作与该Name字段关联的错误的键,就好像Name一个字段直接属于Manager。

如果Employee实现该validation.Validatable接口,我们还可以使用以下代码来验证 Manager,它会生成相同的验证结果:

func (e Employee) Validate() error {
	return validation.ValidateStruct(&e,
		validation.Field(&e.Name, validation.Required),
	)
}

err := validation.ValidateStruct(&m,
	validation.Field(&m.Employee),
	validation.Field(&m.Level, validation.Required),
)
fmt.Println(err)
// Output:
// Level: cannot be blank; Name: cannot be blank.

内置验证规则

validation软件包中提供了以下规则:

  • In(...interface{}):检查是否可以在给定的值列表中找到一个值。

  • Length(min, max int):检查值的长度是否在指定范围内。此规则仅应用于验证字符串,切片,映射和数组。

  • RuneLength(min, max int):检查字符串的长度是否在指定范围内。此规则与Length其他规则类似,不同之处在于,当要验证的值是字符串时,它将检查其符文长度而不是字节长度。

  • Min(min interface{})和Max(max interface{}):检查值是否在指定范围内。这两个规则仅应用于验证int,uint,float和time.Time类型。

  • Match(*regexp.Regexp):检查值是否与指定的正则表达式匹配。此规则仅应用于字符串和字节片。

  • Date(layout string):检查字符串值是否是布局指定格式的日期。通过调用Min()和/或Max(),您可以额外检查日期是否在指定范围内。

  • Required:检查值是否不为空(nil也不为零)。

  • NotNil:检查指针值是否不为nil。非指针值被视为有效。

  • NilOrNotEmpty:检查值是nil指针还是非空值。这与Required将nil指针视为有效指针不同。

  • Skip:这是一条特殊规则,用于指示应跳过其后的所有规则(包括嵌套规则)。

  • MultipleOf:检查该值是否为指定范围的倍数。

  • Each(rules ...Rule):使用其他规则检查可迭代(映射/切片/数组)中的元素。
    该is子包提供了可用于检查的常用字符串的验证规则列表,如果值满足一定要求的格式。请注意,这些规则仅处理字符串和字节片,并且如果字符串或字节片为空,则视为有效。您可以使用Required规则来确保值不为空。以下是该is软件包提供的规则的完整列表:

  • Email:验证字符串是否为电子邮件

  • URL:验证字符串是否为有效网址

  • RequestURL:验证字符串是否为有效的请求网址

  • RequestURI:验证字符串是否为有效的请求URI

  • Alpha:验证字符串是否仅包含英文字母(a-zA-Z)

  • Digit:验证字符串是否仅包含数字(0-9)

  • Alphanumeric:验证字符串是否仅包含英文字母和数字(a-zA-Z0-9)

  • UTFLetter:验证字符串是否仅包含unicode字母

  • UTFDigit:验证字符串是否仅包含unicode十进制数字

  • UTFLetterNumeric:验证字符串是否仅包含unicode字母和数字

  • UTFNumeric:验证字符串是否仅包含unicode数字字符(类别N)

  • LowerCase:验证字符串是否仅包含小写unicode字母

  • UpperCase:验证字符串是否仅包含大写unicode字母

  • Hexadecimal:验证字符串是否为有效的十六进制数字

  • HexColor:验证字符串是否为有效的十六进制颜色代码

  • RGBColor:验证字符串是否为rgb(R,G,B)形式的有效RGB颜色

  • Int:验证字符串是否为有效的整数

  • Float:验证字符串是否为浮点数

  • UUIDv3:验证字符串是否为有效的版本3 UUID

  • UUIDv4:验证字符串是否为有效的版本4 UUID

  • UUIDv5:验证字符串是否为有效的版本5 UUID

  • UUID:验证字符串是否为有效的UUID

  • CreditCard:验证字符串是否为有效的信用卡号

  • ISBN10:验证字符串是否为ISBN版本10

  • ISBN13:验证字符串是否为ISBN版本13

  • ISBN:验证字符串是否为ISBN(版本10或13)

  • JSON:验证字符串是否为有效JSON格式

  • ASCII:验证字符串是否仅包含ASCII字符

  • PrintableASCII:验证字符串是否仅包含可打印的ASCII字符

  • Multibyte:验证字符串是否包含多字节字符

  • FullWidth:验证字符串是否包含全角字符

  • HalfWidth:验证字符串是否包含半角字符

  • VariableWidth:验证字符串是否同时包含全角和半角字符

  • Base64:验证字符串是否在Base64中编码

  • DataURI:验证字符串是否为有效的base64编码数据URI

  • E164:验证字符串是否为有效的E164电话号码(+19251232233)

  • CountryCode2:验证字符串是否为有效的ISO3166 Alpha 2国家/地区代码

  • CountryCode3:验证字符串是否为有效的ISO3166 Alpha 3国家/地区代码

  • DialString:验证字符串是否是可以传递给Dial()的有效拨号字符串

  • MAC:验证字符串是否为MAC地址

  • IP:验证字符串是否为有效的IP地址(版本4或6)

  • IPv4:验证字符串是否为有效的版本4 IP地址

  • IPv6:验证字符串是否为有效的版本6 IP地址

  • Subdomain:验证字符串是否为有效的子域

  • Domain:验证字符串是否为有效域

  • DNSName:验证字符串是否为有效的DNS名称

  • Host:验证字符串是有效的IP(v4和v6)还是有效的DNS名称

  • Port:验证字符串是否为有效的端口号

  • MongoID:验证字符串是否为有效的Mongo ID

  • Latitude:验证字符串是否为有效纬度

  • Longitude:验证字符串是否为有效经度

  • SSN:验证字符串是否为社会安全号码(SSN)

  • Semver:验证字符串是否是有效的语义版本

自定义错误消息

所有内置的验证规则均允许您自定义错误消息。为此,只需调用Error()规则的方法。例如,

data := "2123"
err := validation.Validate(data,
	validation.Required.Error("is required"),
	validation.Match(regexp.MustCompile("^[0-9]{5}$")).Error("must be a string with five digits"),
)
fmt.Println(err)
// Output:
// must be a string with five digits

创建自定义规则

创建自定义规则就像实现validation.Rule界面一样简单。接口包含如下所示的单个方法,该方法应验证值并返回验证错误(如果有):

// Validate validates a value and returns an error if validation fails.
Validate(value interface{}) error

如果您已经具有与上面所示相同的签名的函数,则可以调用validation.By()将其转换为验证规则。例如,

func checkAbc(value interface{}) error {
	s, _ := value.(string)
	if s != "abc" {
		return errors.New("must be abc")
	}
	return nil
}

err := validation.Validate("xyz", validation.By(checkAbc))
fmt.Println(err)
// Output: must be abc

如果您的验证功能使用其他参数,则可以使用以下闭合技巧:

func stringEquals(str string) validation.RuleFunc {
	return func(value interface{}) error {
		s, _ := value.(string)
        if s != str {
            return errors.New("unexpected string")
        }
        return nil
    }
}

err := validation.Validate("xyz", validation.By(stringEquals("abc")))
fmt.Println(err)
// Output: unexpected string

规则组

在多个地方使用多个规则的组合时,可以使用以下技巧来创建规则组,以使代码更易于维护。

var NameRule = []validation.Rule{
	validation.Required,
	validation.Length(5, 20),
}

type User struct {
	FirstName string
	LastName  string
}

func (u User) Validate() error {
	return validation.ValidateStruct(&u,
		validation.Field(&u.FirstName, NameRule...),
		validation.Field(&u.LastName, NameRule...),
	)
}

在上面的示例中,我们创建了一个NameRule包含两个验证规则的规则组。然后,我们使用此规则组同时验证FirstName和LastName。

上下文感知验证

虽然大多数验证规则是独立的,但某些规则可能会动态依赖于上下文。规则可以实现 validation.RuleWithContext接口以支持所谓的上下文感知验证。

要使用上下文验证任意值,请调用validation.ValidateWithContext()。该context.Conext参数将被传递下去,以实现这些规则validation.RuleWithContext。

要使用上下文验证结构的字段,请调用validation.ValidateStructWithContext()。

您可以通过同时实现validation.Rule和来定义上下文感知规则validation.RuleWithContext。您还可以使用validation.WithContext()将功能转换为上下文感知规则。例如,

rule := validation.WithContext(func(ctx context.Context, value interface{}) error {
	if ctx.Value("secret") == value.(string) {
	    return nil
	}
	return errors.New("value incorrect")
})
value := "xyz"
ctx := context.WithValue(context.Background(), "secret", "example")
err := validation.ValidateWithContext(ctx, value, rule)
fmt.Println(err)
// Output: value incorrect

执行上下文感知验证时,如果未实现规则validation.RuleWithContext, validation.Rule则将改用它。

 类似资料: