目录
开篇validator使用方法内置校验器1. Fields2. Network3. Strings4. Formats5. Comparisons6. Other7. 别名错误处理小结开篇
今天继续我们的>
本来打算一节收尾,越写越发现 validator 整体复杂度还是很高的,而且支持了很多场景。可拆解的思路很多,于是打算分成两篇文章来讲。这篇我们会先来了解 validator 的用法,下一篇我们会关注实现的思路和源码解析。
validator
Package>
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 到底提供了哪些能力。
使用方法
我们上一节举的例子就是最简单的场景,在一个>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>
(注:想看完整的建议参考文档 以及仓库 README)
1.>
对于结构体各个属性的校验,这里可以针对一个 field 与另一个 field 相互比较。

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

3.>
字符串相关的校验,用的非常多,比如校验是否是数字,大小写,前后缀等,非常方便。

4.>
符合特定格式,如我们上面提到的 email,信用卡号,颜色,html,base64,json,经纬度,md5 等

5.>
比较大小,用的很多

6.>
杂项,各种通用能力,用的也非常多,我们上面用的 required 就在这一节。包括校验是否为默认值,最大,最小等。

7.>
除了上面的六个大类,还包含两个内部封装的别名校验器,我们已经用过 iscolor,还有国家码:

错误处理
Golang>
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 的实现也非常精巧,只不过内容太多,我们今天暂时覆盖不到,更多关于Go 校验库validator 的资料请关注易采站长站其它相关文章!










