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 里就能用。这里我们简单看一下:

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

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 &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 的用法,其实整体还是非常简洁的,我们只需要全局维护一个 validator 实例,内部会帮我们做好缓存。此后只需要把结构体传入,就可以完成校验,并提供可以解析的错误。

validator 的实现也非常精巧,只不过内容太多,我们今天暂时覆盖不到,更多关于Go 校验库validator 的资料请关注我们其它相关文章!

(0)

相关推荐

  • golang validator参数校验的实现

    今天在改后台页面,参数校验错误时输出全是英文,使用着很难看懂到底时什么错了 故而决定去做i18n前端国际化. 改的时候踩了很多坑,故而记录一下,顺便记录以下查问题的方式. 效果 从原来的Title is required变为标题为必填字段 完成后的代码: 这里主要定义了初始化了一个中文的trans和Validate的变量,并对其做初始化 初始化主要做了以下事情: 注册了TagName函数 // RegisterTagNameFunc registers a function to get alt

  • golan参数校验Validator

    目录 1.实践 1.1校验标签 1.2字符串约束 1.3自定义校验器 前言: 开发接口的时候需要多前端提交的参数进行参数校验,如果提交的参数只有一个两个,这样我们可以简单写个if判断,但是如果提交的参数比较多,通过if判断就比较繁琐了,在Go中有一个validator包可以通过反射结构体struct的tag进行参数校验 1.实践 go get github.com/go-playground/validator/v10 定义结体: type UserInfo struct { ID int `v

  • golang之数据验证validator的实现

    前言 在web应用中经常会遇到数据验证问题,普通的验证方法比较繁琐,这里介绍一个使用比较多的包validator. 原理 将验证规则写在struct对字段tag里,再通过反射(reflect)获取struct的tag,实现数据验证. 安装 go get github.com/go-playground/validator/v10 示例 package main import ( "fmt" "github.com/go-playground/validator/v10&quo

  • golang常用库之字段参数验证库-validator使用详解

    golang常用库:gorilla/mux-http路由库使用 golang常用库:配置文件解析库-viper使用 golang常用库:操作数据库的orm框架-gorm基本使用 golang常用库:字段参数验证库-validator使用 一.背景 在平常开发中,特别是在web应用开发中,为了验证输入字段的合法性,都会做一些验证操作.比如对用户提交的表单字段进行验证,或者对请求的API接口字段进行验证,验证字段的合法性,保证输入字段值的安全,防止用户的恶意请求. 一般的做法是用正则表达式,一个字段

  • golang validator库参数校验实用技巧干货

    目录 validator库参数校验若干实用技巧 基本示例 翻译校验错误提示信息 自定义错误提示信息的字段名 自定义结构体校验方法 自定义字段校验方法 自定义翻译方法 validator库参数校验若干实用技巧 在web开发中一个不可避免的环节就是对请求参数进行校验,通常我们会在代码中定义与请求参数相对应的模型(结构体),借助模型绑定快捷地解析请求中的参数,例如 gin 框架中的Bind和ShouldBind系列方法.本文就以 gin 框架的请求参数校验为例,介绍一些validator库的实用技巧.

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

    目录 开篇 validator 使用方法 内置校验器 1. Fields 2. Network 3. Strings 4. Formats 5. Comparisons 6. Other 7. 别名 错误处理 小结 开篇 今天继续我们的 Golang 经典开源库学习之旅,这篇文章的主角是 validator,Golang 中经典的校验库,它可以让开发者可以很便捷地通过 tag 来控制对结构体字段的校验,使用面非常广泛. 本来打算一节收尾,越写越发现 validator 整体复杂度还是很高的,而且

  • golang常用库之配置文件解析库-viper使用详解

    golang常用库:gorilla/mux-http路由库使用 golang常用库:配置文件解析库-viper使用 golang常用库:操作数据库的orm框架-gorm基本使用 golang常用库:字段参数验证库-validator使用 一.viper简介 viper 配置管理解析库,是由大神 Steve Francia 开发,他在google领导着 golang 的产品开发,他也是 gohugo.io 的创始人之一,命令行解析库 cobra 开发者.总之,他在golang领域是专家,很牛的一个

  • Python enumerate内置库用法解析

    这篇文章主要介绍了Python enumerate内置库用法解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 使用enumerate,可以自动进行索引下标的赋值,本例代码中使用enumerate,进行excel单元格的赋值操作. 代码如果重复被调用,可将该代码封装成类进行使用 import openpyxl #加载excel文件 wb = openpyxl.load_workbook('test_datas/test_cases.xlsx')

  • python统计函数库scipy.stats的用法解析

    背景 总结统计工作中几个常用用法在python统计函数库scipy.stats的使用范例. 正态分布 以正态分布的常见需求为例了解scipy.stats的基本使用方法. 1.生成服从指定分布的随机数 norm.rvs通过loc和scale参数可以指定随机变量的偏移和缩放参数,这里对应的是正态分布的期望和标准差.size得到随机数数组的形状参数.(也可以使用np.random.normal(loc=0.0, scale=1.0, size=None)) In [4]: import numpy a

  • Golang errgroup 设计及实现原理解析

    目录 开篇 errgroup 源码拆解 Group WithContext Wait Go SetLimit TryGo 使用方法 结束语 开篇 继上次学习了信号量 semaphore 扩展库的设计思路和实现之后,今天我们继续来看 golang.org/x/sync 包下的另一个经常被 Golang 开发者使用的大杀器:errgroup. 业务研发中我们经常会遇到需要调用多个下游的场景,比如加载一个商品的详情页,你可能需要访问商品服务,库存服务,券服务,用户服务等,才能从各个数据源获取到所需要的

  • Zend Framework校验器Zend_Validate用法详解

    本文实例讲述了Zend Framework校验器Zend_Validate用法.分享给大家供大家参考,具体如下: 引言: 是对输入内容进行检查,并生成一个布尔结果来表明内容是否被成功校验的机制. 如果isValid()方法返回False,子类的getMessage()方法将返回一个消息数组来解释校验失败的原因. 为了正确地返回消息与错误内容,对于isValid()方法的每次调用,都需要清除前一个isValid()方法调用所导致的消息和错误. 案例: <?php require_once 'Zen

  • Python爬虫PyQuery库基本用法入门教程

    本文实例讲述了Python爬虫PyQuery库基本用法.分享给大家供大家参考,具体如下: PyQuery库也是一个非常强大又灵活的网页解析库,如果你有前端开发经验的,都应该接触过jQuery,那么PyQuery就是你非常绝佳的选择,PyQuery 是 Python 仿照 jQuery 的严格实现.语法与 jQuery 几乎完全相同,所以不用再去费心去记一些奇怪的方法了. 官网地址:http://pyquery.readthedocs.io/en/latest/ jQuery参考文档: http:

  • Golang中反射的常见用法分享

    目录 根据类型做不同处理 标准库 json 中的示例 基本类型的反射 数组类型的反射 chan 反射 map 反射 迭代反射 map 对象 slice 反射 string 反射 interface/Pointer 反射 结构体的反射 遍历结构体字段 根据名称或索引获取结构体字段 修改结构体字段 结构体方法调用 是否实现接口 结构体的 tag 修改结构体未导字段 方法的反射 入参和返回值 通过反射调用方法 总结 在之前的两篇文章 <深入理解 go reflect - 反射基本原理>.<深入

随机推荐