Golang 经典校验库 validator 用法解析
目录
- 开篇
- validator
- 使用方法
- 内置校验器
- 1. Fields
- 2. Network
- 3. Strings
- 4. Formats
- 5. Comparisons
- 6. Other
- 7. 别名
- 错误处理
- 小结
开篇
今天继续我们的 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 都在区间外。我们运行一下看看结果:
<nil>
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 里就能用。这里我们简单看一下:
1. Fields
对于结构体各个属性的校验,这里可以针对一个 field 与另一个 field 相互比较。
2. Network
网络相关的格式校验,可以用来校验 IP 格式,TCP, UDP, URL 等
3. Strings
字符串相关的校验,用的非常多,比如校验是否是数字,大小写,前后缀等,非常方便。
4. Formats
符合特定格式,如我们上面提到的 email,信用卡号,颜色,html,base64,json,经纬度,md5 等
5. Comparisons
比较大小,用的很多
6. Other
杂项,各种通用能力,用的也非常多,我们上面用的 required 就在这一节。包括校验是否为默认值,最大,最小等。
7. 别名
除了上面的六个大类,还包含两个内部封装的别名校验器,我们已经用过 iscolor,还有国家码:
错误处理
Golang 的 error 是个 interface,默认其实只提供了 Error() 这一个方法,返回一个字符串,能力比较鸡肋。同样的,validator 返回的错误信息也是个字符串:
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag
这样当然不错,但问题在于,线上环境下,很多时候我们并不是【人工地】来阅读错误信息,这里的 error 最终是要转化成错误信息展现给用户,或者打点上报的。
我们需要有能力解析出来,是哪个结构体的哪个属性有问题,哪个 tag 拦截了。怎么办?
其实 validator 返回的类型底层是 validator.ValidationErrors
,我们可以在判空之后,用它来进行类型断言,将 error 类型转化过来再判断:
err := validate.Struct(mystruct) validationErrors := err.(validator.ValidationErrors)
底层的结构我们看一下:
// ValidationErrors is an array of FieldError's // for use in custom error messages post validation. type ValidationErrors []FieldError // Error is intended for use in development + debugging and not intended to be a production error message. // It allows ValidationErrors to subscribe to the Error interface. // All information to create an error message specific to your application is contained within // the FieldError found within the ValidationErrors array func (ve ValidationErrors) Error() string { buff := bytes.NewBufferString("") var fe *fieldError for i := 0; i < len(ve); i++ { fe = ve[i].(*fieldError) buff.WriteString(fe.Error()) buff.WriteString("\n") } return strings.TrimSpace(buff.String()) }
这里可以看到,所谓 ValidationErrors 其实一组 FieldError,所谓 FieldError 就是每一个属性的报错,我们的 ValidationErrors 实现的 func Error() string
方法,也是将各个 fieldError(对 FieldError 接口的默认实现)连接起来,最后 TrimSpace 清掉空格展示。
在我们拿到了 ValidationErrors 后,可以遍历各个 FieldError,拿到业务需要的信息,用来做日志打印/打点上报/错误码对照等,这里是个 interface,大家各取所需即可:
// FieldError contains all functions to get error details type FieldError interface { // Tag returns the validation tag that failed. if the // validation was an alias, this will return the // alias name and not the underlying tag that failed. // // eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla" // will return "iscolor" Tag() string // ActualTag returns the validation tag that failed, even if an // alias the actual tag within the alias will be returned. // If an 'or' validation fails the entire or will be returned. // // eg. alias "iscolor": "hexcolor|rgb|rgba|hsl|hsla" // will return "hexcolor|rgb|rgba|hsl|hsla" ActualTag() string // Namespace returns the namespace for the field error, with the tag // name taking precedence over the field's actual name. // // eg. JSON name "User.fname" // // See StructNamespace() for a version that returns actual names. // // NOTE: this field can be blank when validating a single primitive field // using validate.Field(...) as there is no way to extract it's name Namespace() string // StructNamespace returns the namespace for the field error, with the field's // actual name. // // eq. "User.FirstName" see Namespace for comparison // // NOTE: this field can be blank when validating a single primitive field // using validate.Field(...) as there is no way to extract its name StructNamespace() string // Field returns the fields name with the tag name taking precedence over the // field's actual name. // // eq. JSON name "fname" // see StructField for comparison Field() string // StructField returns the field's actual name from the struct, when able to determine. // // eq. "FirstName" // see Field for comparison StructField() string // Value returns the actual field's value in case needed for creating the error // message Value() interface{} // Param returns the param value, in string form for comparison; this will also // help with generating an error message Param() string // Kind returns the Field's reflect Kind // // eg. time.Time's kind is a struct Kind() reflect.Kind // Type returns the Field's reflect Type // // eg. time.Time's type is time.Time Type() reflect.Type // Translate returns the FieldError's translated error // from the provided 'ut.Translator' and registered 'TranslationFunc' // // NOTE: if no registered translator can be found it returns the same as // calling fe.Error() Translate(ut ut.Translator) string // Error returns the FieldError's message Error() string }
小结
今天我们了解了 validator 的用法,其实整体还是非常简洁的,我们只需要全局维护一个 validator 实例,内部会帮我们做好缓存。此后只需要把结构体传入,就可以完成校验,并提供可以解析的错误。
validator 的实现也非常精巧,只不过内容太多,我们今天暂时覆盖不到,更多关于Go 校验库validator 的资料请关注我们其它相关文章!