热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Golang 经典校验库 validator 用法解析

Golang 经典校验库 validator 用法解析-目录开篇validator使用方法内置校验器1.Fields2.Network3.Strings4.Formats5.Com

开篇

今天继续我们的 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: "#",
		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 

这里可以看到,所谓 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 实例,内部会帮我们做好缓存。此后只需要把结构体传入,就可以完成校验,并提供可以解析的错误。


推荐阅读
  • Go GUIlxn/walk 学习3.菜单栏和工具栏的具体实现
    本文介绍了使用Go语言的GUI库lxn/walk实现菜单栏和工具栏的具体方法,包括消息窗口的产生、文件放置动作响应和提示框的应用。部分代码来自上一篇博客和lxn/walk官方示例。文章提供了学习GUI开发的实际案例和代码示例。 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • baresip android编译、运行教程1语音通话
    本文介绍了如何在安卓平台上编译和运行baresip android,包括下载相关的sdk和ndk,修改ndk路径和输出目录,以及创建一个c++的安卓工程并将目录考到cpp下。详细步骤可参考给出的链接和文档。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了一个适用于PHP应用快速接入TRX和TRC20数字资产的开发包,该开发包支持使用自有Tron区块链节点的应用场景,也支持基于Tron官方公共API服务的轻量级部署场景。提供的功能包括生成地址、验证地址、查询余额、交易转账、查询最新区块和查询交易信息等。详细信息可参考tron-php的Github地址:https://github.com/Fenguoz/tron-php。 ... [详细]
  • Gitlab接入公司内部单点登录的安装和配置教程
    本文介绍了如何将公司内部的Gitlab系统接入单点登录服务,并提供了安装和配置的详细教程。通过使用oauth2协议,将原有的各子系统的独立登录统一迁移至单点登录。文章包括Gitlab的安装环境、版本号、编辑配置文件的步骤,并解决了在迁移过程中可能遇到的问题。 ... [详细]
  • 本文介绍了如何在Azure应用服务实例上获取.NetCore 3.0+的支持。作者分享了自己在将代码升级为使用.NET Core 3.0时遇到的问题,并提供了解决方法。文章还介绍了在部署过程中使用Kudu构建的方法,并指出了可能出现的错误。此外,还介绍了开发者应用服务计划和免费产品应用服务计划在不同地区的运行情况。最后,文章指出了当前的.NET SDK不支持目标为.NET Core 3.0的问题,并提供了解决方案。 ... [详细]
  • 本文介绍了在go语言中利用(*interface{})(nil)传递参数类型的原理及应用。通过分析Martini框架中的injector类型的声明,解释了values映射表的作用以及parent Injector的含义。同时,讨论了该技术在实际开发中的应用场景。 ... [详细]
  • 使用在线工具jsonschema2pojo根据json生成java对象
    本文介绍了使用在线工具jsonschema2pojo根据json生成java对象的方法。通过该工具,用户只需将json字符串复制到输入框中,即可自动将其转换成java对象。该工具还能解析列表式的json数据,并将嵌套在内层的对象也解析出来。本文以请求github的api为例,展示了使用该工具的步骤和效果。 ... [详细]
  • 本文介绍了在多平台下进行条件编译的必要性,以及具体的实现方法。通过示例代码展示了如何使用条件编译来实现不同平台的功能。最后总结了只要接口相同,不同平台下的编译运行结果也会相同。 ... [详细]
  • 本文由编程笔记小编整理,主要介绍了使用Junit和黄瓜进行自动化测试中步骤缺失的问题。文章首先介绍了使用cucumber和Junit创建Runner类的代码,然后详细说明了黄瓜功能中的步骤和Steps类的实现。本文对于需要使用Junit和黄瓜进行自动化测试的开发者具有一定的参考价值。摘要长度:187字。 ... [详细]
  • 如何使用Python从工程图图像中提取底部的方法?
    本文介绍了使用Python从工程图图像中提取底部的方法。首先将输入图片转换为灰度图像,并进行高斯模糊和阈值处理。然后通过填充潜在的轮廓以及使用轮廓逼近和矩形核进行过滤,去除非矩形轮廓。最后通过查找轮廓并使用轮廓近似、宽高比和轮廓区域进行过滤,隔离所需的底部轮廓,并使用Numpy切片提取底部模板部分。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
author-avatar
天秤aaaaaaa_150
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有