简介
ozzo-validation
是一个十分弱小的,灵便的数据校验库。与其余基于 struct tag 的数据校验库不同,ozzo-validation
认为 struct tag 在应用过程中比拟容易出错。因为 struct tag 实质上就是字符串,齐全基于字符串的解析,无奈利用语言的动态查看机制,很容易在人不知;鬼不觉中写错而不易觉察,理论代码中呈现谬误也很难排查。
ozzo-validation
提倡用代码指定规定来进行校验。实际上ozzo
是辅助开发 Web 应用程序的一套框架,包含 ORM 库ozzo-dbx
,路由库ozzo-routing
,日志库ozzo-log
,配置库ozzo-config
以及最闻名的,应用最为宽泛的数据校验库ozzo-validation
。作者甚至还搞出了一个开发 Web 应用程序的模版go-rest-api
。
疾速应用
本文代码应用 Go Modules。
创立目录并初始化:
$ mkdir ozzo-validation && cd ozzo-validation$ go mod init github.com/darjun/go-daily-lib/ozzo-validation
装置ozzo-validation
库:
$ go get -u github.com/go-ozzo/ozzo-validation/v4
ozzo-validation
的程序写起来都比拟直观:
package mainimport ( "fmt" "github.com/go-ozzo/ozzo-validation/v4/is" "github.com/go-ozzo/ozzo-validation/v4")func main() { name := "darjun" err := validation.Validate(name, validation.Required, validation.Length(2, 10), is.URL) fmt.Println(err)}
ozzo-validation
应用函数Validate()
对根本类型值进行校验,传入的第一个参数就是要校验的数据,前面以可变参数传入一个或多个校验规定。上例中对一个字符串做校验。咱们用代码来表白规定:
validation.Required
:示意值必须设置,对于字符串来说就是不能为空;validation.Length(2, 10)
:指定长度的范畴;is.URL
:is
子包中内置了大量的辅助办法,is.URL
限度值必须是 URL 格局。
Validate()
函数依据传入的规定按程序顺次对数据进行校验,直到遇到某个规定校验失败,或所有规定都校验胜利。如果一个规定返回失败,则跳过前面的规定间接返回谬误。如果数据通过了所有规定,则返回一个nil
。
运行下面程序输入:
must be a valid URL
因为字符串"darjun"显著不是一个非法的 URL。如果去掉is.URL
规定,则运行输入nil
。
构造体
应用ValidateStruct()
函数能够对一个构造体对象进行校验。咱们须要顺次指定构造体中各个字段的校验规定:
type User struct { Name string Age int Email string}func validateUser(u *User) error { err := validation.ValidateStruct(u, validation.Field(&u.Name, validation.Required, validation.Length(2, 10)), validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)), validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email)) return err}
ValidateStruct()
承受一个构造体的指针作为第一个参数,前面顺次指定各个字段的规定。字段规定应用validation.Field()
函数指定,该函数承受一个指向具体字段的指针,后跟一个或多个规定。下面咱们限度,名字长度在[2, 10]之间,年龄在[1, 200]之间(权且认为当初人类最多能活 200 年),电子邮箱长度在[10, 50]之间,并且应用is.Email
限度它必须是一个非法的邮箱地址。同时这 3 个字段都是必填的(用validation.Required
限度的)。
而后咱们结构一个非法的User
对象和一个非法的User
对象,别离校验:
func main() { u1 := &User { Name: "darjun", Age: 18, Email: "darjun@126.com", } fmt.Println("user1:", validateUser(u1)) u2 := &User { Name: "lidajun12345", Age: 201, Email: "lidajun's email", } fmt.Println("user2:", validateUser(u2))}
程序运行输入:
user1: <nil>user2: Age: must be no greater than 200; Email: must be a valid email address; Name: the length must be between 2 and 10.
对于构造体来说,validation
顺次对每个字段测验传入的规定。对于某个字段,如果一条规定校验失败了,则跳过前面的规定,持续校验下一个字段。如果某个字段校验失败,会在后果中蕴含对于该字段的错误信息,如上例。
Map
有时数据保留在一个map
中,而非一个构造体中。这时能够应用validation.Map()
指定校验map
的规定,validation.Map()
规定中须要应用validation.Key()
顺次指定各个键对应的一个或多个规定。最初将map
类型的数据和validation.Map()
规定传给validation.Validate()
函数校验:
func validateUser(u map[string]interface{}) error { err := validation.Validate(u, validation.Map( validation.Key("name", validation.Required, validation.Length(2, 10)), validation.Key("age", validation.Required, validation.Min(1), validation.Max(200)), validation.Key("email", validation.Required, validation.Length(10, 50), is.Email), )) return err}func main() { u1 := map[string]interface{} { "name": "darjun", "age": 18, "email": "darjun@126.com", } fmt.Println("user1:", validateUser(u1)) u2 := map[string]interface{} { "name": "lidajun12345", "age": 201, "email": "lidajun's email", } fmt.Println("user2:", validateUser(u2))}
咱们革新了下面的例子,改用map[string]interface{}
存储User
信息。map
的校验与构造体相似,依据validation.Map()
中指定的键的程序顺次校验。如果某个键校验失败,记录错误信息。最终汇总所有键的错误信息返回。运行程序:
user1: <nil>user2: age: must be no greater than 200; email: must be a valid email address; name: the length must be between 2 and 10.
可校验类型
ozzo-validation
库提供了一个接口Validatable
:
type Validatable interface { // Validate validates the data and returns an error if validation fails. Validate() error}
但凡实现了Validatable
接口的类型都是可校验的类型。validation.Validate()
函数在校验某个类型的数据时,先校验传入该函数的所有规定。如果这些规定都通过了,那么Validate()
函数判断该类型有没有实现Validatbale
接口。如果实现了,则调用其Validate()
办法进行校验。咱们让上例中User
类型实现Validatable
接口:
type User struct { Name string Age int Gender string Email string}func (u *User) Validate() error { err := validation.ValidateStruct(u, validation.Field(&u.Name, validation.Required, validation.Length(2, 10)), validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)), validation.Field(&u.Gender, validation.Required, validation.In("male", "female")), validation.Field(&u.Email, validation.Required, validation.Length(10, 50), is.Email)) return err}
因为User
实现了Validatable
接口,咱们能够间接调用Validate()
函数校验:
func main() { u1 := &User{ Name: "darjun", Age: 18, Gender: "male", Email: "darjun@126.com", } fmt.Println("user1:", validation.Validate(u1, validation.NotNil)) u2 := &User{ Name: "lidajun12345", Age: 201, Email: "lidajun's email", } fmt.Println("user2:", validation.Validate(u2, validation.NotNil))}
在通过了NotNil
校验后,Validate()
函数还会调用User.Validate()
办法进行校验。
须要留神的是,在实现了Validatable
接口的类型的Validate()
办法外部,不能间接对该类型的值调用validation.Validate()
函数,这会导致有限递归:
type UserName stringfunc (n UserName) Validate() error { return validation.Validate(n, validation.Required, validation.Length(2, 10))}func main() { var n1, n2 UserName = "dj", "lidajun12345" fmt.Println("username1:", validation.Validate(n1)) fmt.Println("username2:", validation.Validate(n2))}
咱们基于string
定义了一个新类型UserName
,规定UserName
非空,并且长度在[2, 10]范畴内。然而下面的Validate()
办法中将UserName
类型的变量n
传入了函数validation.Validate()
。该函数外部查看发现UserName
实现了Validatable
接口,又会调用它的Validate()
办法,导致有限递归。
咱们只须要简略地将n
转为string
类型即可:
func (n UserName) Validate() error { return validation.Validate(string(n), validation.Required, validation.Length(2, 10))}
可校验类型的汇合
Validate()
函数对元素为可校验类型(即实现了Validatable
接口)的汇合(切片/数组/map等)进行校验时,会顺次调用其元素的Validate()
办法,最初校验返回一个validation.Errors
类型。这实际上是一个map[string]error
类型。键为元素的键(对于切片和数组就是索引,对于map
就是键),值为谬误值。例:
func main() { u1 := &User{ Name: "darjun", Age: 18, Gender: "male", Email: "darjun@126.com", } u2 := &User{ Name: "lidajun12345", Age: 201, Email: "lidajun's email", } userSlice := []*User{u1, u2} userMap := map[string]*User{ "user1": u1, "user2": u2, } fmt.Println("user slice:", validation.Validate(userSlice)) fmt.Println("user map:", validation.Validate(userMap))}
userSlice
切片中第二个元素的校验谬误会在后果的键1
(索引)中返回,userMap
中键user2
校验的谬误会在后果的键user2
中返回。运行后果:
user slice: 1: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).user map: user2: (Age: must be no greater than 200; Email: must be a valid email address; Gender: cannot be blank; Name: the length must be between 2 and 10.).
如果须要汇合中每个元素都满足某些规定,咱们能够应用validation.Each()
函数。例如,咱们的User
对象有多个邮箱,要求每个邮箱地址的格局都非法:
type User struct { Name string Age int Emails []string}func (u *User) Validate() error { return validation.ValidateStruct(u, validation.Field(&u.Emails, validation.Each(is.Email)))}func main() { u := &User{ Name: "dj", Age: 18, Emails: []string{ "darjun@126.com", "don't know", }, } fmt.Println(validation.Validate(u))}
谬误音讯中会指出哪个地位数据不非法了:
Emails: (1: must be a valid email address.).
条件规定
咱们能够依据某个字段的值来给另一个字段设置规定。例如咱们的User
对象有两个字段:布尔值Student
示意是否还是学生,字符串School
示意学校。在Student
为true
时,字段School
必须存在并且长度在[10, 20]范畴内:
type User struct { Name string Age int Student bool School string}func (u *User) Validate() error { return validation.ValidateStruct(u, validation.Field(&u.Name, validation.Required, validation.Length(2, 10)), validation.Field(&u.Age, validation.Required, validation.Min(1), validation.Max(200)), validation.Field(&u.School, validation.When(u.Student, validation.Required, validation.Length(10, 20))))}func main() { u1 := &User{ Name: "dj", Age: 18, Student: true, } u2 := &User{ Name: "lidajun", Age: 31, } fmt.Println("user1:", validation.Validate(u1)) fmt.Println("user2:", validation.Validate(u2))}
咱们应用validation.When()
函数,该函数承受一个布尔值作为第一个参数,一个或多个规定作为前面的可变参数。只有在第一个参数为true
是才执行前面的规定校验。
u1
因为设置了字段Student
为true
,所以School
字段不能为空。u2
因为Student=false
,School
字段可有可无。运行:
user1: School: cannot be blank.user2: <nil>
在查看注册用户信息时,咱们确保用户必须设置了邮箱或手机号也能够用条件规定:
type User struct { Email string Phone string}func (u *User) Validate() error { return validation.ValidateStruct(u, validation.Field(&u.Email, validation.When(u.Phone == "", validation.Required.Error("Either email or phone is required."), is.Email)), validation.Field(&u.Phone, validation.When(u.Email == "", validation.Required.Error("Either email or phone is required."), is.Alphanumeric)))}func main() { u1 := &User{} u2 := &User{ Email: "darjun@126.com", } u3 := &User{ Phone: "17301251652", } u4 := &User{ Email: "darjun@126.com", Phone: "17301251652", } fmt.Println("user1:", validation.Validate(u1)) fmt.Println("user2:", validation.Validate(u2)) fmt.Println("user3:", validation.Validate(u3)) fmt.Println("user4:", validation.Validate(u4))}
如果Phone
字段为空,Email
必须设置。反之,如果Email
字段为空,Phone
必须设置。所有的规定都能够调用Error()
办法设置自定义错误信息。运行输入:
user1: Email: Either email or phone is required.; Phone: Either email or phone is required..user2: <nil>user3: <nil>user4: <nil>
自定义规定
除了库提供的规定之外,咱们还能够定义本人的规定。规定实现为一个如下类型的函数:
func Validate(value interface{}) error
上面咱们实现一个查看 IP 地址是否非法的函数。这里咱们介绍一个库commonregex
。这个库收录了绝大部分罕用的正则表达式。我之前也写过一篇文章介绍这个库的应用,Go 每日一库之 commonregex,感兴趣能够过来看看。
func checkIP(value interface{}) error { ip, ok := value.(string) if !ok { return errors.New("ip must be string") } ipList := commonregex.IPs(ip) if len(ipList) != 1 || ipList[0] != ip { return errors.New("invalid ip format") } return nil}
而后定义一个网络地址构造及校验办法,通过validation.By()
函数应用自定义的校验函数:
type Addr struct { IP string Port int}func (a *Addr) Validate() error { return validation.ValidateStruct(a, validation.Field(&a.IP, validation.Required, validation.By(checkIP)), validation.Field(&a.Port, validation.Min(1024), validation.Max(65536)))}
验证:
func main() { a1 := &Addr{ IP: "127.0.0.1", Port: 6666, } a2 := &Addr{ IP: "xxx.yyy.zzz.hhh", Port: 7777, } fmt.Println("addr1:", validation.Validate(a1)) fmt.Println("addr2:", validation.Validate(a2))}
运行:
addr1: <nil>addr2: IP: invalid ip format.
规定组
每次指定规定都一个一个地来指定有点不不便,这时咱们能够将罕用的校验规定组成一个规定组,须要时间接应用这个组即可。例如,咱们我的项目中约定非法的用户名必须是 ASCII 字母加数字,长度为 10-20,用户名必定不能为空。规定组没什么非凡的,它只是一个规定的切片:
var NameRule = []validation.Rule{ validation.Required, is.Alphanumeric, validation.Length(10, 20),}func main() { name1 := "lidajun12345" name2 := "lidajun@!#$%" name3 := "short" name4 := "looooooooooooooooooong" fmt.Println("name1:", validation.Validate(name1, NameRule...)) fmt.Println("name2:", validation.Validate(name2, NameRule...)) fmt.Println("name3:", validation.Validate(name3, NameRule...)) fmt.Println("name4:", validation.Validate(name4, NameRule...))}
运行:
name1: <nil>name2: must contain English letters and digits onlyname3: the length must be between 10 and 20name4: the length must be between 10 and 20
总结
ozzo-validation
提倡以代码指定规定代替容易出错的struct tag
,并提供了大量的内置规定。应用ozzo-validation
编写的代码清晰,易读,而且对编译器敌对(很多谬误都裸露在编译期)。本文介绍了ozzo-validation
库的根本应用,外围就两个函数Validate()
和ValidateStruct()
,前者用于校验根本类型或可校验的类型,后者用于校验构造体。理论编码过程中,个别都会让构造体实现Validatbale
接口将它变为可校验类型,再调用Validate()
函数校验。
ozzo-validation
还能够对汇合进行校验,能够自定义校验规定,能够定义通用的校验组。除此之外,ozzo-validation
还有很多高级个性,如自定义谬误,基于context.Context
的校验,应用正则表达式定义规定等,感兴趣可自行摸索。
大家如果发现好玩、好用的 Go 语言库,欢送到 Go 每日一库 GitHub 上提交 issue
参考
- ozzo-validation GitHub:github.com/go-ozzo/ozzo-validation
- go-rest-api GitHub:github.com/qiangxue/go-rest-api
- Go 每日一库之 commonregex:https://darjun.github.io/2020/09/05/godailylib/commonregex/
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~