Golang经典校验库validator用法解析

2022-08-26 13:02:15
目录
开篇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 &lt; 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 的资料请关注易采站长站其它相关文章!