您现在的位置是:网站首页> 编程资料编程资料

Golang 经典校验库 validator 用法解析_Golang_

2023-05-26 414人已围观

简介 Golang 经典校验库 validator 用法解析_Golang_

开篇

今天继续我们的 Golang 经典开源库学习之旅,这篇文章的主角是 validator,Golang 中经典的校验库,它可以让开发者可以很便捷地通过 tag 来控制对结构体字段的校验,使用面非常广泛。

本来打算一节收尾,越写越发现 validator 整体复杂度还是很高的,而且支持了很多场景。可拆解的思路很多,于是打算分成两篇文章来讲。这篇我们会先来了解 validator 的用法,下一篇我们会关注实现的思路和源码解析。

validator

Package validator implements value validations for structs and individual fields based on tags.

validator 是一个结构体参数验证器。

它提供了【基于 tag 对结构体以及单独属性的校验能力】。经典的 gin 框架就是用了 validator 作为默认的校验器。它的能力能够帮助开发者最大程度地减少【基础校验】的代码,你只需要一个 tag 就能完成校验。完整的文档参照 这里

目前 validator 最新版本已经升级到了 v10,我们可以用

go get github.com/go-playground/validator/v10 

添加依赖后,import 进来即可

import "github.com/go-playground/validator/v10" 

我们先来看一个简单的例子,了解 validator 能怎样帮助开发者完成校验。

package main import ( "fmt" "github.com/go-playground/validator/v10" ) type User struct { Name string `validate:"min=6,max=10"` Age int `validate:"min=1,max=100"` } func main() { validate := validator.New() u1 := User{Name: "lidajun", Age: 18} err := validate.Struct(u1) fmt.Println(err) u2 := User{Name: "dj", Age: 101} err = validate.Struct(u2) fmt.Println(err) } 

这里我们有一个 User 结构体,我们希望 Name 这个字符串长度在 [6, 10] 这个区间内,并且希望 Age 这个数字在 [1, 100] 区间内。就可以用上面这个 tag。

校验的时候只需要三步:

  • 调用 validator.New() 初始化一个校验器;
  • 将【待校验的结构体】传入我们的校验器的 Struct 方法中;
  • 校验返回的 error 是否为 nil 即可。

上面的例子中,lidajun 长度符合预期,18 这个 Age 也在区间内,预期 err 为 nil。而第二个用例 Name 和 Age 都在区间外。我们运行一下看看结果:


Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
Key: 'User.Age' Error:Field validation for 'Age' failed on the 'max' tag

这里我们也可以看到,validator 返回的报错信息包含了 Field 名称 以及 tag 名称,这样我们也容易判断哪个校验没过。

如果没有 tag,我们自己手写的话,还需要这样处理:

func validate(u User) bool { if u.Age < 1 || u.Age > 100 { return false } if len(u.Name) < 6 || len(u.Name) > 10 { return false } return true } 

乍一看好像区别不大,其实一旦结构体属性变多,校验规则变复杂,这个校验函数的代价立刻会上升,另外你还要显示的处理报错信息,以达到上面这样清晰的效果(这个手写的示例代码只返回了一个 bool,不好判断是哪个没过)。

越是大结构体,越是规则复杂,validator 的收益就越高。我们还可以把 validator 放到中间件里面,对所有请求加上校验,用的越多,效果越明显。

其实笔者个人使用经验来看,validator 带来的另外两个好处在于:

  • 因为需要经常使用校验能力,养成了习惯,每定义一个结构,都事先想好每个属性应该有哪些约束,促使开发者思考自己的模型。这一点非常重要,很多时候我们就是太随意定义一些结构,没有对应的校验,结果导致各种脏数据,把校验逻辑一路下沉;
  • 有了 tag 来描述约束规则,让结构体本身更容易理解,可读性,可维护性提高。一看结构体,扫几眼 tag 就知道业务对它的预期。

这两个点虽然比较【意识流】,但在开发习惯上还是很重要的。

好了,到目前只是浅尝辄止,下面我们结合示例看看 validator 到底提供了哪些能力。

使用方法

我们上一节举的例子就是最简单的场景,在一个 struct 中定义好 validate:"xxx" tag,然后调用校验器的 err := validate.Struct(user) 方法来校验。

这一节我们结合实例来看看最常用的场景下,我们会怎样用 validator:

package main import ( "fmt" "github.com/go-playground/validator/v10" ) // User contains user information 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... } // Address houses a users address information type Address struct { Street string `validate:"required"` City string `validate:"required"` Planet string `validate:"required"` Phone string `validate:"required"` } // use a single instance of Validate, it caches struct info var validate *validator.Validate func main() { validate = validator.New() validateStruct() validateVariable() } func validateStruct() { address := &Address{ Street: "Eavesdown Docks", Planet: "Persphone", Phone: "none", } user := &User{ FirstName: "Badger", LastName: "Smith", Age: 135, Email: "Badger.Smith@gmail.com", FavouriteColor: "#000-", Addresses: []*Address{address}, } // returns nil or ValidationErrors ( []FieldError ) err := validate.Struct(user) if err != nil { // this check is only needed when your code could produce // an invalid value for validation such as interface with nil // value most including myself do not usually have code like this. if _, ok := err.(*validator.InvalidValidationError); ok { fmt.Println(err) return } for _, err := range err.(validator.ValidationErrors) { fmt.Println(err.Namespace()) fmt.Println(err.Field()) fmt.Println(err.StructNamespace()) fmt.Println(err.StructField()) fmt.Println(err.Tag()) fmt.Println(err.ActualTag()) fmt.Println(err.Kind()) fmt.Println(err.Type()) fmt.Println(err.Value()) fmt.Println(err.Param()) fmt.Println() } // from here you can create your own error messages in whatever language you wish return } // save user to database } func validateVariable() { myEmail := "joeybloggs.gmail.com" errs := validate.Var(myEmail, "required,email") if errs != nil { fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "email" tag return } // email ok, move on } 

仔细观察你会发现,第一步永远是创建一个校验器,一个 validator.New() 解决问题,后续一定要复用,内部有缓存机制,效率比较高。

关键在第二步,大体上分为两类:

  • 基于结构体调用 err := validate.Struct(user) 来校验;
  • 基于变量调用 errs := validate.Var(myEmail, "required,email")

结构体校验这个相信看完这个实例,大家已经很熟悉了。

变量校验这里很有意思,用起来确实简单,大家看 validateVariable 这个示例就 ok,但是,但是,我只有一个变量,我为啥还要用这个 validator 啊?

原因很简单,不要以为 validator 只能干一些及其简单的,比大小,比长度,判空逻辑。这些非常基础的校验用一个 if 语句也搞定。

validator 支持的校验规则远比这些丰富的多。

我们先把前面示例的结构体拿出来,看看支持哪些 tag:

// User contains user information 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... } // Address houses a users address information type Address struct { Street string `validate:"required"` City string `validate:"required"` Planet string `validate:"required"` Phone string `validate:"required"` } 

格式都是 validate:"xxx",这里不再说,关键是里面的配置。

validator 中如果你针对同一个 Field,有多个校验项,可以用下面两种运算符:

  • , 逗号表示【与】,即每一个都需要满足;
  • | 表示【或】,多个条件满足一个即可。

我们一个个来看这个 User 结构体出现的 tag:

  • required 要求必须有值,不为空;
  • gte=0,lte=130 其中 gte 代表大于等于,lte 代表小于等于,这个语义是 [0,130] 区间;
  • required, emal 不仅仅要有值,还得符合 Email 格式;
  • iscolor 后面注释也提了,这是个别名,本质等价于 hexcolor|rgb|rgba|hsl|hsla,属于 validator 自带的别名能力,符合这几个规则任一的,我们都认为属于表示颜色。
  • required,dive,required 这个 dive 大有来头,注意这个 Addresses 是个 Address 数组,我们加 tag 一般只是针对单独的数据类型,这种【容器型】的怎么办?

这时 dive 的能力就派上用场了。

dive 的语义在于告诉 validator 不要停留在我这一级,而是继续往下校验,无论是 slice, array 还是 map,校验要用的 tag 就是在 dive 之后的这个。

这样说可能不直观,我们来看一个例子:

[][]string with validation tag "gt=0,dive,len=1,dive,required" // gt=0 will be applied to [] // len=1 will be applied to []string // required will be applied to string 

第一个 gt=0 适用于最外层的数组,出现 dive 后,往下走,len=1 作为一个 tag 适用于内层的 []string,此后又出现 dive,继续往下走,对于最内层的每个 string,要求每个都是 required。

[][]string with validation tag "gt=0,dive,dive,required" // gt=0 will be applied to [] // []string will be spared validation // required will be applied to string 

第二个例子,看看能不能理解?

其实,只要记住,每次出现 dive,都往里面走就 ok。

回到我们一开始的例子:

Addresses []*Address validate:"required,dive,required"

表示的意思是,我们要求 Addresses 这个数组是 required,此外对于每个元素,也得是 required。

内置校验器

validator 对于下面六种场景都提供了丰富的校验器,放到 tag 里就能用。这里我们简单看一下:

(注:想看完整的建议参考文档 以及仓库 README

1. Fields

对于结构体各个属性的校验,这里可以针对一个 field 与另一个 field 相互比较。

2. Network

网络相关的格式校验,可以用来校验 IP 格式,TCP, UDP, URL 等

3. Strings提示: 本文由整理自网络,如有侵权请联系本站删除!
本站声明:
1、本站所有资源均来源于互联网,不保证100%完整、不提供任何技术支持;
2、本站所发布的文章以及附件仅限用于学习和研究目的;不得将用于商业或者非法用途;否则由此产生的法律后果,本站概不负责!

-六神源码网