validator 开源仓库地址:https://github.com/go-playground/validator
本文基于 validator v10.14.1 版本实现数据验证,自定义验证,自定义错误信息,错误信息翻译的实现。
才疏学浅,如有错误,欢迎指正。
1. 介绍与安装
1.1 介绍
Validator 是一个 Go 语言的开源库,用于对结构体字段进行验证。它支持许多常见的验证规则,例如必填、最小长度、最大长度、正则表达式等。使用 Validator 可以帮助开发者快速简洁地验证结构体字段,从而提高代码质量和安全性。
Validator 的使用非常简单,只需在结构体字段上添加验证规则即可。例如,以下代码验证 Name 字段必须是一个非空字符串:
type User struct {
    Name string `validate:"required"`
}
要验证结构体,只需调用 validator.ValidateStruct() 方法。例如,以下代码验证 User 结构体:
user := User{
    Name: "John Doe",
}
err := validator.ValidateStruct(&user)
if err != nil {
    fmt.Println(err)
}
如果 User 结构体是有效的,则 ValidateStruct() 方法不会返回任何错误。如果 User 结构体无效,则 ValidateStruct() 方法会返回一个包含错误信息的 ValidationErrors 结构体。
Validator 是一个非常强大的验证库,支持许多常见的验证规则。使用 Validator 可以帮助开发者快速简洁地验证结构体字段,从而提高代码质量和安全性。
1.2 安装
go get github.com/go-playground/validator/v10
2. 快速开始
2.1 基本使用
2.1.1 变量验证
package main
import "github.com/go-playground/validator/v10"
var validate *validator.Validate
func main() {
	validate = validator.New()
	// 验证变量
	email := "joeybloggs.gmail.com"
	randomString := "1234567890"
	err := validate.Var(email, "required,email")
	if err != nil {
		println("email:" + err.Error())
	}
	err = validate.Var(randomString, "required,email")
	if err != nil {
		println("random" + err.Error())
	}
}
首先创建validator实例化对象,然后调用validate.Var()方法进行验证,第一个参数是需要验证的变量,第二个参数是验证规则,多个规则用逗号分隔。
如果不出意外,你会看到如下输出:
randomKey: '' Error:Field validation for '' failed on the 'email' tag
上述错误信息的意思是:randomKey字段验证失败,因为randomKey字段的值不是一个有效的邮箱地址。
2.1.2 结构体验证
package main
import "github.com/go-playground/validator/v10"
// 用户
type User struct {
	FirstName      string     `validate:"required"`
	LastName       string     `validate:"required"`
	Age            uint8      `validate:"gte=0,lte=130"`
	Email          string     `validate:"required,email"`
	FavouriteColor string     `validate:"iscolor"`                // alias for 'hexcolor|rgb|rgba|hsl|hsla'
	Addresses      []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
// 地址
type Address struct {
	Street string `validate:"required"`
	City   string `validate:"required"`
	Planet string `validate:"required"`
	Phone  string `validate:"required"`
}
var validate *validator.Validate
func main() {
	validate = validator.New()
	user := User{}
	err := validate.Struct(user)
	if err != nil {
		println(err.Error())
	}
}
上文定义了结构体Address、User,其中User结构体包含了一个Address结构体的切片。validate.Struct()方法可以验证结构体,如果结构体中包含了其他结构体,那么需要使用dive关键字进行递归验证。
如果不出意外,你会看到如下输出:
Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag
Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'required' tag
Key: 'User.Email' Error:Field validation for 'Email' failed on the 'required' tag
Key: 'User.FavouriteColor' Error:Field validation for 'FavouriteColor' failed on the 'iscolor' tag
Key: 'User.Addresses' Error:Field validation for 'Addresses' failed on the 'required' tag
上述错误信息的意思是:User结构体中的FirstName字段验证失败,因为FirstName字段的值是一个空字符串。其他的错误信息也是类似的。
2.1.3 内置验证规则
Fields:
| Tag | Description | 
|---|---|
| eqcsfield | Field Equals Another Field (relative) | 
| eqfield | Field Equals Another Field | 
| fieldcontains | Check the indicated characters are present in the Field | 
| fieldexcludes | Check the indicated characters are not present in the field | 
| gtcsfield | Field Greater Than Another Relative Field | 
| gtecsfield | Field Greater Than or Equal To Another Relative Field | 
| gtefield | Field Greater Than or Equal To Another Field | 
| gtfield | Field Greater Than Another Field | 
| ltcsfield | Less Than Another Relative Field | 
| ltecsfield | Less Than or Equal To Another Relative Field | 
| ltefield | Less Than or Equal To Another Field | 
| ltfield | Less Than Another Field | 
| necsfield | Field Does Not Equal Another Field (relative) | 
| nefield | Field Does Not Equal Another Field | 
Network:
| Tag | Description | 
|---|---|
| cidr | Classless Inter-Domain Routing CIDR | 
| cidrv4 | Classless Inter-Domain Routing CIDRv4 | 
| cidrv6 | Classless Inter-Domain Routing CIDRv6 | 
| datauri | Data URL | 
| fqdn | Full Qualified Domain Name (FQDN) | 
| hostname | Hostname RFC 952 | 
| hostname_port | HostPort | 
| hostname_rfc1123 | Hostname RFC 1123 | 
| ip | Internet Protocol Address IP | 
| ip4_addr | Internet Protocol Address IPv4 | 
| ip6_addr | Internet Protocol Address IPv6 | 
| ip_addr | Internet Protocol Address IP | 
| ipv4 | Internet Protocol Address IPv4 | 
| ipv6 | Internet Protocol Address IPv6 | 
| mac | Media Access Control Address MAC | 
| tcp4_addr | Transmission Control Protocol Address TCPv4 | 
| tcp6_addr | Transmission Control Protocol Address TCPv6 | 
| tcp_addr | Transmission Control Protocol Address TCP | 
| udp4_addr | User Datagram Protocol Address UDPv4 | 
| udp6_addr | User Datagram Protocol Address UDPv6 | 
| udp_addr | User Datagram Protocol Address UDP | 
| unix_addr | Unix domain socket end point Address | 
| uri | URI String | 
| url | URL String | 
| http_url | HTTP URL String | 
| url_encoded | URL Encoded | 
| urn_rfc2141 | Urn RFC 2141 String | 
Strings:
| Tag | Description | 
|---|---|
| alpha | Alpha Only | 
| alphanum | Alphanumeric | 
| alphanumunicode | Alphanumeric Unicode | 
| alphaunicode | Alpha Unicode | 
| ascii | ASCII | 
| boolean | Boolean | 
| contains | Contains | 
| containsany | Contains Any | 
| containsrune | Contains Rune | 
| endsnotwith | Ends Not With | 
| endswith | Ends With | 
| excludes | Excludes | 
| excludesall | Excludes All | 
| excludesrune | Excludes Rune | 
| lowercase | Lowercase | 
| multibyte | Multi-Byte Characters | 
| number | Number | 
| numeric | Numeric | 
| printascii | Printable ASCII | 
| startsnotwith | Starts Not With | 
| startswith | Starts With | 
| uppercase | Uppercase | 
Format:
| Tag | Description | 
|---|---|
| base64 | Base64 String | 
| base64url | Base64URL String | 
| base64rawurl | Base64RawURL String | 
| bic | Business Identifier Code (ISO 9362) | 
| bcp47_language_tag | Language tag (BCP 47) | 
| btc_addr | Bitcoin Address | 
| btc_addr_bech32 | Bitcoin Bech32 Address (segwit) | 
| credit_card | Credit Card Number | 
| mongodb | MongoDB ObjectID | 
| cron | Cron | 
| datetime | Datetime | 
| e164 | e164 formatted phone number | 
| E-mail String | |
| eth_addr | Ethereum Address | 
| hexadecimal | Hexadecimal String | 
| hexcolor | Hexcolor String | 
| hsl | HSL String | 
| hsla | HSLA String | 
| html | HTML Tags | 
| html_encoded | HTML Encoded | 
| isbn | International Standard Book Number | 
| isbn10 | International Standard Book Number 10 | 
| isbn13 | International Standard Book Number 13 | 
| iso3166_1_alpha2 | Two-letter country code (ISO 3166-1 alpha-2) | 
| iso3166_1_alpha3 | Three-letter country code (ISO 3166-1 alpha-3) | 
| iso3166_1_alpha_numeric | Numeric country code (ISO 3166-1 numeric) | 
| iso3166_2 | Country subdivision code (ISO 3166-2) | 
| iso4217 | Currency code (ISO 4217) | 
| json | JSON | 
| jwt | JSON Web Token (JWT) | 
| latitude | Latitude | 
| longitude | Longitude | 
| luhn_checksum | Luhn Algorithm Checksum (for strings and (u)int) | 
| postcode_iso3166_alpha2 | Postcode | 
| postcode_iso3166_alpha2_field | Postcode | 
| rgb | RGB String | 
| rgba | RGBA String | 
| ssn | Social Security Number SSN | 
| timezone | Timezone | 
| uuid | Universally Unique Identifier UUID | 
| uuid3 | Universally Unique Identifier UUID v3 | 
| uuid3_rfc4122 | Universally Unique Identifier UUID v3 RFC4122 | 
| uuid4 | Universally Unique Identifier UUID v4 | 
| uuid4_rfc4122 | Universally Unique Identifier UUID v4 RFC4122 | 
| uuid5 | Universally Unique Identifier UUID v5 | 
| uuid5_rfc4122 | Universally Unique Identifier UUID v5 RFC4122 | 
| uuid_rfc4122 | Universally Unique Identifier UUID RFC4122 | 
| md4 | MD4 hash | 
| md5 | MD5 hash | 
| sha256 | SHA256 hash | 
| sha384 | SHA384 hash | 
| sha512 | SHA512 hash | 
| ripemd128 | RIPEMD-128 hash | 
| ripemd128 | RIPEMD-160 hash | 
| tiger128 | TIGER128 hash | 
| tiger160 | TIGER160 hash | 
| tiger192 | TIGER192 hash | 
| semver | Semantic Versioning 2.0.0 | 
| ulid | Universally Unique Lexicographically Sortable Identifier ULID | 
| cve | Common Vulnerabilities and Exposures Identifier (CVE id) | 
Comparisons:
| Tag | Description | 
|---|---|
| eq | Equals | 
| eq_ignore_case | Equals ignoring case | 
| gt | Greater than | 
| gte | Greater than or equal | 
| lt | Less Than | 
| lte | Less Than or Equal | 
| ne | Not Equal | 
| ne_ignore_case | Not Equal ignoring case | 
Other:
| Tag | Description | 
|---|---|
| dir | Existing Directory | 
| dirpath | Directory Path | 
| file | Existing File | 
| filepath | File Path | 
| image | Image | 
| isdefault | Is Default | 
| len | Length | 
| max | Maximum | 
| min | Minimum | 
| oneof | One Of | 
| required | Required | 
| required_if | Required If | 
| required_unless | Required Unless | 
| required_with | Required With | 
| required_with_all | Required With All | 
| required_without | Required Without | 
| required_without_all | Required Without All | 
| excluded_if | Excluded If | 
| excluded_unless | Excluded Unless | 
| excluded_with | Excluded With | 
| excluded_with_all | Excluded With All | 
| excluded_without | Excluded Without | 
| excluded_without_all | Excluded Without All | 
| unique | Unique | 
Aliases:
| Tag | Description | 
|---|---|
| iscolor | hexcolor|rgb|rgba|hsl|hsla | 
| country_code | iso3166_1_alpha2|iso3166_1_alpha3|iso3166_1_alpha_numeric | 
2.2 自定义验证器
package main
import (
	"github.com/go-playground/validator/v10"
	"regexp"
)
var validate *validator.Validate
func IsPhone(fl validator.FieldLevel) bool {
	// 如果是空值则跳过验证
	if fl.Field().String() == "" {
		return true
	}
	// 正则验证手机号
	matched, _ := regexp.MatchString(`^1[3456789]\d{9}$`, fl.Field().String())
	return matched
}
func main() {
	validate = validator.New()
	err := validate.RegisterValidation("isphone", IsPhone)
	if err != nil {
		panic(err)
	}
	randomStr := "1234567890"
	err = validate.Var(randomStr, "isphone")
	if err != nil {
		println(err.Error())
	}
}
上文中的自定义验证器IsPhone,其实就是一个函数,函数的签名如下:
func IsPhone(fl validator.FieldLevel) bool
当然,你也可以使用结构体的方法来实现自定义验证器,只需要将函数签名改为:
func (s *Struct) IsPhone(fl validator.FieldLevel) bool
创建validator实例以后将自定义验证器注册进去即可:
err := validate.RegisterValidation("isphone", IsPhone)
RegisterValidation方法接收两个参数,第一个参数是自定义验证器的名称,第二个参数是自定义验证器的实现,
注册成功后,就可以在验证器中使用了:
如果操作无误的话,你应该会看到如下输出:
Key: '' Error:Field validation for '' failed on the 'isphone' tag
2.3 翻译验证器错误信息
package main
import (
	"github.com/go-playground/locales/zh"
	ut "github.com/go-playground/universal-translator"
	"github.com/go-playground/validator/v10"
	zhTranslation "github.com/go-playground/validator/v10/translations/zh"
	"reflect"
	"regexp"
)
var validate *validator.Validate
var trans ut.Translator
func IsPhone(fl validator.FieldLevel) bool {
	// 如果是空值则跳过验证
	if fl.Field().String() == "" {
		return true
	}
	// 正则验证手机号
	matched, _ := regexp.MatchString(`^1[3456789]\d{9}$`, fl.Field().String())
	return matched
}
func initTranslate() {
	//初始化错误翻译器
	trans, _ = ut.New(zh.New()).GetTranslator("zh")
	validate = validator.New()
	//使用fld.Tag.Get("comment")注册一个获取tag的自定义方法以实现翻译Field
	validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
		return fld.Tag.Get("tag")
	})
	err := validate.RegisterValidation("isphone", IsPhone)
	if err != nil {
		panic(err)
	}
	_ = validate.RegisterTranslation("isphone", trans, func(ut ut.Translator) error {
		return ut.Add("isphone", "{0}该参数不是一个手机号", true)
	}, func(ut ut.Translator, fe validator.FieldError) string {
		t, _ := ut.T("isphone", fe.Field())
		return t
	})
	err = zhTranslation.RegisterDefaultTranslations(validate, trans)
	if err != nil {
		println("注册错误翻译器失败:" + err.Error())
	}
	println("注册错误翻译器成功")
}
type User struct {
	Phone string `json:"phone" validate:"required,isphone" tag:"手机号"`
}
func main() {
	initTranslate()
	err := validate.Struct(User{
		Phone: "0123456789",
	})
	if err != nil {
		err := err.(validator.ValidationErrors)
		for _, e := range err.Translate(trans) {
			println(e)
		}
	}
}
上文中的initTranslate方法,主要是初始化错误翻译器,这里使用的是zh语言包,你也可以使用其他语言包,具体可以参考这里。
我们还是使用上面自定义验证器isphone,只不过这次我们将错误信息翻译成中文,你可以看到输出如下:
注册错误翻译器成功
手机号该参数不是一个手机号
注意翻译器翻译时不包括 Field 字段,因此我们需要在翻译器中添加 {0} 占位符,这样就可以将 Field 字段的值传入翻译器中了。
RegisterTagNameFunc函数的作用是注册一个获取tag的自定义方法以实现翻译Field,这里我们使用了tag作为json的别名,因此我们需要在tag中添加tag字段,这样就可以将tag字段的值传入翻译器中了。
至此,我们已经完成了验证器的基本使用,gin中使用验证器与上述一致。