解析 Golang 经典校验库 validator 设计和原理

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第31天,点击查看活动详情

开篇

上一篇我们介绍了基础用法,相信对于大多数开发者已经足够了,不熟悉的同学可以再看一下我们的用法篇:解析 Golang 经典校验库 validator 用法

这一篇我们不会再过多讲怎么用,而是从一个开发者的角度思考,怎么设计出来一个校验库。

validator 整体的扩展性还是非常好的,结构很不错,是个优秀的学习模板。我们来看看到底有什么好的设计,思路是我们可以借鉴,在日常开发中使用的。

今天不会只是简单的摆源码,加注释,而是希望大家跟着我的思路,假定我们现在需要设计实现一个 validator 库,应该怎么做。

回到起点

image.png

我们坐着时光机回到 2015 年,那是 validator 这个项目发布第一个 production ready 版本 1.0 的时候,当时我们使用的路径还是 import "gopkg.in/joeybloggs/go-validate-yourself.v1"。匆匆 7 年已逝,validator 目前支持的各种能力,扩展越来越多,但最核心的基本原理还是没有变化的。

看的开源库越多,越有一种体会,现在大家越来越追求【功能上的丰富】,为了很多边边角角场景的适配而把代码搞的很复杂。其实最核心,大家最想要的那个能力,很可能是很简单,很直接的。所以,从学习的角度上看,我会比较建议大家看【简化版】的实现 + 最新的接口文档。

先理解最核心,最本质的那个能力,然后多看看文档,理解目前提供的最全,最新的功能,知道武器库里有什么可以用的家伙,省的以后自己随便造轮子。随后的部分就很灵活了,如果你的场景确实需要高阶功能,再仔细过一遍链路看看这个功能怎么实现的。

回到正题,下来我们参照着 1.0 的代码,设身处地思考,理解设计者们的用意。

推导实现

试想一下,如果让你实现一个 validator,业务给结构体打 tag,随后调用 ValidateStruct() 就实现一些格式校验,你会怎么做?

这一节我们试着运用一些 Golang 常见的思路和工具,来拼凑出来整个 big picture,看看怎样实现这样一个 validator 的内核。这里不会涉及实际代码,思路上先要想清楚。

解析结构体

首先我们的 ValidateStruct 函数拿到的是个结构体,而我们的校验是在 field(属性)的维度(当然,目前 validator 已经支持了直接针对结构体类型来校验,但这不是我们当下的重点,暂时认为我们的诉求就是实现 field 维度的校验)。

怎么办呢?

还是祭出反射大法,我们可以通过 reflect.TypeOf 来获取类型,reflect.ValueOf 获取值,然后通过 NumField 来遍历各个 field 就可以拿到相关信息了。

另外还可能有一个异常 case,传入的 ValidateStruct 这个签名的入参势必是个 interface{},那么理论上讲使用方可以传进来任何类型。所以针对指针的场景,我们也需要适配。如果既不是结构体,也不是指针,也不是个接口类型,这种按照我们核心场景来评估,目前不用支持,可以报错,或者 panic。

校验 field

  • 第一步,当然就是要解析出来 tag,知道用户在每个 field 的 tag 里写了什么。

这一点很好做,我们在此前的 Golang 反射实战 中已经介绍过如何用反射获取 tag。不熟悉的同学建议再看下,这里我们直接贴 demo:

package main

import (
	"fmt"
	"reflect"
)

type MyStruct struct {
	Location string `customTag:"custom value"`
}

func main() {
	t := reflect.TypeOf(MyStruct{})
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag
		fmt.Printf("tag=%+v\n", tag)

		customTagVal := tag.Get("customTag")
		fmt.Printf("customTagVal=%s\n", customTagVal)

		lookVal, exist := tag.Lookup("customTag")
		fmt.Printf("lookVal=%s, exist=%v\n", lookVal, exist)
	}
}
复制代码

简单说,先拿到 reflect.Type,然后通过 NumField 拿到 index,再通过 Field(index) 方法就可以拿到这个 Field 所有的信息,tag 就在这里。

  • 第二步,有了 tag,要识别出来是否是我们这个 validator 的标签,解析出来实际我们依赖的部分,而不是傻傻的拿着 json 或者 msgp 的标签做逻辑。

这里就需要我们首先定义一个属于 validator 的标签名,类似 json:"xxx" 一样,我们的格式应该类似 validate:"xxx",这个 validate 字符串就是我们的标签名,这个也可以允许随后开发者自己扩展,通过一些 option 选项来做。

我们姑且把这个标签名定义叫做 tagName,那么获取内容很简单:field.Tag.Get(tagName) 即可。

  • 第三步,解析开发者标记的校验器逻辑,我们支持 - 标识不校验,| 标识多个校验器的【或】,, 指的是【与】,首先就要判断出来这个整体结构上的关系,如果是 , 或者是 |,我们还要解析出具体开发者要求执行的校验器有哪些。

这里可能一上来我们匹配一下 - 或者 omitempty,先把基础校验做了,然后是不是可以 strings.Split 分隔一下,看看命中了哪个,就知道整体的逻辑关系了。应该不会很复杂。

  • 第四步,现在我们知道了整体的【与】【或】逻辑,以及具体的每个校验器,比如 len, min, lte, email, url 等,下一步就是调用每个校验器函数,得到校验结果返回。

实现校验器

好,目前我们主流程似乎是能跑通的,下面就要看我们希望怎么设计校验器了,也就是 "omitempty,min=1,max=10" 这些一个个的校验逻辑,怎么实现并集成到我们的 validator 中。

一个很直观的想法是,维护一个 map

  • key 代表校验器的值,比如上面这些 min,max;
  • value 代表实际的校验器函数。

这个 map 可以维护在我们的 validator 实例中,这样用户可能注册它自己的校验器函数进来。

函数的签名呢?我们至少需要两个部分:

  1. 参数值,比如要实现 min 对应的校验器,你肯定得给我 min=1 这里面的 1,我才知道怎么比较;
  2. 当前列的类型和值。

可能类似这样:func(fieldVal reflect.Value, param string) bool,给我这个 field 的值,以及校验器的参数,我返回给你是否校验通过。

错误处理

从需求上看我们只需要返回哪一个 field 遇到了哪个 tag 校验器失败了就 ok。所以上面返回 bool 就够了,不需要再详细的信息。

我们可以自己构造一个类似 ValidationError 的结构体用来承载错误信息,类似这样:

type ValidationError struct { StructName string FieldName string FailedTag string Param string }

知道了结构体类型,field 是哪个,以及在哪个校验器失败,入参是什么,从需求上来说就够了。

但这个结构是每一个 (field, tag) 的组合就会有一个,所以需要思考如何打包成统一的 error 信息返回,代表整个 struct 校验失败的错误信息。

推导小结

好了,到目前为止,比较核心的部分我们都推导了一遍,先思考,随后看代码才会有收获。

整体推导来看,实现每一部分并不复杂,下一节我们会结合代码看看 validator 是怎样实现的。这个版本其实是不支持 dive,exist, 结构体维度校验等高阶功能的,但完全可上 production,也是真正 validator 的核心。后面大部分还是为了新功能适配的逻辑,大家感兴趣的话可以在学完这篇文章后,再看看最新源码理解一下。

源码解读

感兴趣的同学可以查看一下从 v1 到现在 v10 的 release,本节我们的代码解析基于 v1 版本,重在解读好的设计。

这个版本的 validator 还是托管在 "github.com/joeybloggs/go-validate-yourself" 路径下。代码其实非常简单,很适合大家学习。

image.png

可以看到,真正起到实际作用的,只有三个文件:

  • baked_in.go 维护了所有默认【校验器】的实现;
  • regexes.gp 维护用到的正则表达式;
  • validator.go 核心业务逻辑。

下面我们来好好学习一下源码,看初版的 validator 是怎样实现我们上面推导的流程。

校验函数定义

// ValidationFunc that accepts the value of a field and parameter for use in validation (parameter not always used or needed)
type ValidationFunc func(v interface{}, param string) bool
复制代码

这就是我们说过多次的校验器的签名,入参包括属性值(the value of a field),以及参数值,后者是可选的。

在 baked_in.go 中其实维护了一个全局的 map,跟我们预期匹配,所有校验器都注册在这里:

image.png

所谓 bake in,指的就是系统自带的,默认提供的能力。所以下面这个 map 其实提供的就是 validator 默认的校验器,作为用户我们也可以提供自定义的校验器,注册进来,随后会过到,这里先不要着急。

// BakedInValidators is the map of ValidationFunc used internally
// but can be used with any new Validator if desired
var BakedInValidators = map[string]ValidationFunc{
	"required":    hasValue,
	"len":         hasLengthOf,
	"min":         hasMinOf,
	"max":         hasMaxOf,
	"lt":          isLt,
	"lte":         isLte,
	"gt":          isGt,
	"gte":         isGte,
	"alpha":       isAlpha,
	"alphanum":    isAlphanum,
	"numeric":     isNumeric,
	"number":      isNumber,
	"hexadecimal": isHexadecimal,
	"hexcolor":    isHexcolor,
	"rgb":         isRgb,
	"rgba":        isRgba,
	"hsl":         isHsl,
	"hsla":        isHsla,
	"email":       isEmail,
	"url":         isURL,
	"uri":         isURI,
}
复制代码

其实个人觉得这一批校验器就是 validator 的核心,后来新增的太多新的校验器,以及功能,边际效益是递减的。最常用的场景都在这里了。

我们简单抽几个看一看:

  • required

对应 hasValue 函数,如果是容器类型,就看 len 是否大于0,如果是单独的类型,调用 reflect.Zero 加上 field 进行校验。

func hasValue(field interface{}, param string) bool {

	st := reflect.ValueOf(field)

	switch st.Kind() {

	case reflect.Slice, reflect.Map, reflect.Array:
		return field != nil && int64(st.Len()) > 0

	default:
		return field != nil && field != reflect.Zero(reflect.TypeOf(field)).Interface()
	}
}
复制代码

这里要重点说一下,一定要记住最后 default 里面这一行 return field != nil && field != reflect.Zero(reflect.TypeOf(field)).Interface(),这是校验是否为空的经典解法,日常开发也用得上。不要以为简单比较一下是否为 nil 就完事。这里再 interface 的场景是有坑的,详细参照我们此前的文章 Golang 中的 nil 用法解析

当然,这里我们也可以用最经典的

func isNil(i interface{}) bool {
    return i == nil || reflect.ValueOf(i).IsNil()
}
复制代码

但 IsNil 本身在一些场景下是会 panic 的,并不稳,建议还是用这里的写法来处理 interface 与 nil 的比较。

  • gt

语义是 greater than,即大于。代码很好理解,将 param 转为数字,通过反射拿到 field 的值。

如果两边都是数字,就直接比大小。如果 field 是个 string,或是 array,slice 这种容器,就拿 length 来比较长度。

func isGt(field interface{}, param string) bool {

	st := reflect.ValueOf(field)

	switch st.Kind() {

	case reflect.String:
		p := asInt(param)

		return int64(len(st.String())) > p

	case reflect.Slice, reflect.Map, reflect.Array:
		p := asInt(param)

		return int64(st.Len()) > p

	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		p := asInt(param)

		return st.Int() > p

	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
		p := asUint(param)

		return st.Uint() > p

	case reflect.Float32, reflect.Float64:
		p := asFloat(param)

		return st.Float() > p

	default:
		panic(fmt.Sprintf("Bad field type %T", field))
	}
}
复制代码
  • email

校验邮件地址格式,这里的 emailRegex 就维护在我们此前提到的 regexes.go 中,只是匹配一个正则,很简单。

func isEmail(field interface{}, param string) bool {

	st := reflect.ValueOf(field)

	switch st.Kind() {

	case reflect.String:
		return emailRegex.MatchString(field.(string))
	default:
		panic(fmt.Sprintf("Bad field type %T", field))
	}
}
复制代码

Validator 实例

// Validator implements the Validator Struct
// NOTE: Fields within are not thread safe and that is on purpose
// Functions Tags etc. should all be predifined before use, so subscribe to the philosiphy
// or make it thread safe on your end
type Validator struct {
	// TagName being used.
	tagName string
	// validationFuncs is a map of validation functions and the tag keys
	validationFuncs map[string]ValidationFunc
}

// var bakedInValidators = map[string]ValidationFunc{}

var internalValidator = NewValidator(defaultTagName, BakedInValidators)

// NewValidator creates a new Validator instance
// NOTE: it is not necessary to create a new validator as the internal one will do in 99.9% of cases, but the option is there.
func NewValidator(tagName string, funcs map[string]ValidationFunc) *Validator {
	return &Validator{
		tagName:         tagName,
		validationFuncs: funcs,
	}
}
复制代码

这里很关键,我觉得至少第一版代码看起来还是很干净的。我们肯定是需要一个 Validator 结构体来承载数据的。这里有两个成员变量:

  1. tagName:其实就是我们默认的 validate:"xxx" 里面的这个 validate,理论上讲是可以换的,所以你会发现对外暴露的 NewValidator 函数里允许传这个值,只不过大家一般不动它;

  2. validationFuncs:这就是绑定在这个 Validator 实例上的校验函数集合。

结构很干净,validationFuncs 作为绑定到实例级别的函数集合,不仅仅支持 bake in 这批默认的函数,还应该支持用户自定义的校验函数。

怎么做到这一点呢?

从上面的代码我们可以看到,validator 库维护了一个全局的 Validator 实例,叫做 internalValidator。默认的 tagName 为 validate,默认的校验函数就是前面我们见到的 BakedInValidators:

var internalValidator = NewValidator(defaultTagName, BakedInValidators)

这样使用者默认情况下,是不需要手动再来调用一次 validate.NewValidator() 方法,可以直接调用函数校验,这里也可以看到,本质还是调用默认的这个 Validator 的同名方法:

// ValidateStruct validates a struct and returns a struct containing the errors
func ValidateStruct(s interface{}) *StructValidationErrors {
	return internalValidator.ValidateStruct(s)
}
复制代码

用法上同样支持两种:

  1. 使用默认的校验器
errs := validator.ValidateStruct(//your struct)
valErr := validator.ValidateFieldByTag(field, "omitempty,min=1,max=10")
复制代码

这个时候不代表校验函数不能变,我们依然可以用 AddFunction 来增加自定义的校验函数

// AddFunction adds a ValidationFunc to the baked in Validator's map of validators denoted by the key
func AddFunction(key string, f ValidationFunc) error {
	return internalValidator.AddFunction(key, f)
}
复制代码
  1. 自己新定义 Validator
newValidator = validator.New("struct tag name", validator.BakedInFunctions)
复制代码

校验流程

核心在于 ValidateStruct 以及 ValidateFieldByTag 这两个 Validator 的方法是怎样实现的。我们分别来追踪一下,看跟我们的推导预期是否一致。

我们先来看看 ValidateFieldByTag,因为本质,底层还是依赖每个 field 的校验,搞清楚单个 field 是怎么做的,ValidateStruct 就很容易理解了。

ValidateFieldByTag

// ValidateFieldByTag allows validation of a single field with the internal validator, still using tag style validation to check multiple errors
func ValidateFieldByTag(f interface{}, tag string) *FieldValidationError {
	return internalValidator.validateFieldByNameAndTag(f, "", tag)
}

// ValidateFieldByTag allows validation of a single field, still using tag style validation to check multiple errors
func (v *Validator) ValidateFieldByTag(f interface{}, tag string) *FieldValidationError {
	return v.validateFieldByNameAndTag(f, "", tag)
}
复制代码

一个一个来,先看入口,这里会发现底层调用的是 validateFieldByNameAndTag 子方法的能力,传入了 field 值 f,以及 tag,中间这个空字符串对应的是 field 的名称。

这里用空字符串也能理解,毕竟我们传入的只是个变量,可能是个 string, int 不可能通过这些来判断出来这个 field 的名称,所以置空。

关键在于 validateFieldByNameAndTag 这个方法,我们给了一个值,也给了 tag,预期它能返回我们校验结果。下面看看是怎么实现的:

func (v *Validator) validateFieldByNameAndTag(f interface{}, name string, tag string) *FieldValidationError {

	// This is a double check if coming from ValidateStruct but need to be here in case function is called directly
	if tag == "-" {
		return nil
	}

	if strings.Contains(tag, omitempty) && !hasValue(f, "") {
		return nil
	}

	valueField := reflect.ValueOf(f)

	if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
		return v.ValidateFieldByTag(valueField.Elem().Interface(), tag)
	}

	switch valueField.Kind() {

	case reflect.Struct, reflect.Interface, reflect.Invalid:
		panic("Invalid field passed to ValidateFieldWithTag")
	}

	var valErr *FieldValidationError
	var err error
	valTags := strings.Split(tag, ",")

	for _, valTag := range valTags {

		orVals := strings.Split(valTag, "|")

		if len(orVals) > 1 {

			errTag := ""

			for _, val := range orVals {

				valErr, err = v.validateFieldByNameAndSingleTag(f, name, val)

				if err == nil {
					return nil
				}

				errTag += "|" + valErr.ErrorTag

			}

			errTag = strings.TrimLeft(errTag, "|")

			valErr.ErrorTag = errTag
			valErr.Kind = valueField.Kind()

			return valErr
		}

		if valErr, err = v.validateFieldByNameAndSingleTag(f, name, valTag); err != nil {
			valErr.Kind = valueField.Kind()

			return valErr
		}
	}

	return nil
}
复制代码

这里是重头戏,我们好好来看看,将上面的代码拆分成一个个小部分解析:

  1. 上来显示校验是否走了 - 以及 omitempty 两个 tag,这里也是调用了我们上面展示过的 hasValue 函数校验,比较简单的 case 优先处理掉;
// This is a double check if coming from ValidateStruct but need to be here in case function is called directly
if tag == "-" {
	return nil
}

if strings.Contains(tag, omitempty) && !hasValue(f, "") {
	return nil
}
复制代码
  1. 用 reflect.ValueOf 将 interface{} 转成 reflect.Value,这样我们就能进行各种操作了,这一点是很自然的处理。这里也兼容了指针的场景
valueField := reflect.ValueOf(f)

if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
	return v.ValidateFieldByTag(valueField.Elem().Interface(), tag)
}
复制代码

如果是指针,且不为 nil,就通过 Elem() 拿到真正指向的值,递归调用自身。

  1. 校验类型,这里我们只希望是单独的 field,单独的标量才能处理,针对 reflect.Struct, reflect.Interface, reflect.Invalid 这三种不合法的过滤掉;
switch valueField.Kind() {

case reflect.Struct, reflect.Interface, reflect.Invalid:
	panic("Invalid field passed to ValidateFieldWithTag")
}
复制代码
  1. 好了,到这里校验 ok 了,我们开始处理 tag,还记得么,我们需要先区分开到底是个【或】还是【与】。所以这里采用了跟我们预期一样的 strings.Split,这样就拿到了所有必须满足的 tag,进入循环。这里每一个 valTag,我们都期望它必须校验通过(这一级是【与】)。
valTags := strings.Split(tag, ",")
for _, valTag := range valTags {
	
}
复制代码
  1. 那【或】怎么办呢?其实是在这个循环内,又嵌套了一个类似的操作。
orVals := strings.Split(valTag, "|")

if len(orVals) > 1 {

	errTag := ""

	for _, val := range orVals {

		valErr, err = v.validateFieldByNameAndSingleTag(f, name, val)

		if err == nil {
			return nil
		}

		errTag += "|" + valErr.ErrorTag

	}

	errTag = strings.TrimLeft(errTag, "|")

	valErr.ErrorTag = errTag
	valErr.Kind = valueField.Kind()

	return valErr
}
复制代码

如果【或】解析出来有多个 tag,进入 len > 1 的分支,此时就会拿着这些【或】语义的 tag 挨个检查。

注意区别,这里如果 err == nil,是直接就 return nil 了,这也符合我们的语义,只要有一个校验通过,就认为整体通过。

如果到最后,发现走出了 for 循环还没 return,说明每一个子 tag 都没通过,这个时候我们将相关错误信息组装后返回。

  1. 在【与】的循环里,处理完【或】的逻辑后,下来继续判断当前 tag 是否校验通过:
if valErr, err = v.validateFieldByNameAndSingleTag(f, name, valTag); err != nil {
	valErr.Kind = valueField.Kind()

	return valErr
}
复制代码

这个 validateFieldByNameAndSingleTag 我们下面会说,目前只需要理解为,这是针对单独的一个 tag 进行 field 校验的方法。

注意,这里一旦 err != nil,就会返回一个 valErr,这也符合【与】的语义。

  1. 如果在【与】的循环里没有 return 发生,就到了最后,会直接返回一个 nil,结束处理。

这个方法主要是调度,每一步都不复杂,是个很好的范例,对于【与】和【或】两种模式的处理很有参考性。

下面我们来看真正完成校验的重头戏 validateFieldByNameAndSingleTag

func (v *Validator) validateFieldByNameAndSingleTag(f interface{}, name string, valTag string) (*FieldValidationError, error) {

	vals := strings.Split(valTag, "=")
	key := strings.Trim(vals[0], " ")

	if len(key) == 0 {
		panic(fmt.Sprintf("Invalid validation tag on field %s", name))
	}

	valErr := &FieldValidationError{
		Field:    name,
		ErrorTag: key,
		Value:    f,
		Param:    "",
	}

	// OK to continue because we checked it's existance before getting into this loop
	if key == omitempty {
		return valErr, nil
	}

	valFunc, ok := v.validationFuncs[key]
	if !ok {
		panic(fmt.Sprintf("Undefined validation function on field %s", name))
	}

	param := ""
	if len(vals) > 1 {
		param = strings.Trim(vals[1], " ")
	}

	if err := valFunc(f, param); !err {
		valErr.Param = param
		return valErr, errors.New(key)
	}

	return valErr, nil
}
复制代码

这里很简单,我们就不拆小块代码了,直接分析:

  1. 先用 = 来 split,拿到我们的 tag 对应的 key 和 value,比如 min=5,这里就能拆解出来 key: min, value: 5;
  2. 发现解析出来 key 是空的,说明传入了非法参数,panic;
  3. 提前构建错误信息,这个结构关注一下:
// FieldValidationError contains a single fields validation error
type FieldValidationError struct {
	Field    string
	ErrorTag string
	Kind     reflect.Kind
	Param    string
	Value    interface{}
}

// This is intended for use in development + debugging and not intended to be a production error message.
// it also allows FieldValidationError to be used as an Error interface
func (e *FieldValidationError) Error() string {
	return fmt.Sprintf(validationFieldErrMsg, e.Field, e.ErrorTag)
}
复制代码

其实跟我们的定义很像,只是扩充了 Kind,Value,这样确实会增强对错误的上下文判断。

  1. 从 Validator 的 validationFuncs 中根据 key 来获取真正的校验函数并执行,精华都在这几行:
valFunc, ok := v.validationFuncs[key]
if err := valFunc(f, param); !err {
        valErr.Param = param
        return valErr, errors.New(key)
}
复制代码

执行如果失败,把 param 赋值给 FieldValidationError 然后返回即可。

ValidateStruct

了解了单独 field 的校验能力,现在我们转过头来看看对结构体是怎么校验的,其实就很简单了。底层依赖的还是 validateFieldByNameAndTag 的能力,前面我们已经分析过了。

// ValidateStruct validates a struct and returns a struct containing the errors
func (v *Validator) ValidateStruct(s interface{}) *StructValidationErrors {

	structValue := reflect.ValueOf(s)
	structType := reflect.TypeOf(s)
	structName := structType.Name()

	validationErrors := &StructValidationErrors{
		Struct:       structName,
		Errors:       map[string]*FieldValidationError{},
		StructErrors: map[string]*StructValidationErrors{},
	}

	if structValue.Kind() == reflect.Ptr && !structValue.IsNil() {
		return v.ValidateStruct(structValue.Elem().Interface())
	}

	if structValue.Kind() != reflect.Struct && structValue.Kind() != reflect.Interface {
		panic("interface passed for validation is not a struct")
	}

	var numFields = structValue.NumField()

	for i := 0; i < numFields; i++ {

		valueField := structValue.Field(i)
		typeField := structType.Field(i)

		if valueField.Kind() == reflect.Ptr && !valueField.IsNil() {
			valueField = valueField.Elem()
		}

		tag := typeField.Tag.Get(v.tagName)

		if tag == "-" {
			continue
		}

		// if no validation and not a struct (which may containt fields for validation)
		if tag == "" && valueField.Kind() != reflect.Struct && valueField.Kind() != reflect.Interface {
			continue
		}

		switch valueField.Kind() {

		case reflect.Struct, reflect.Interface:

			if !unicode.IsUpper(rune(typeField.Name[0])) {
				continue
			}

			if structErrors := v.ValidateStruct(valueField.Interface()); structErrors != nil {
				validationErrors.StructErrors[typeField.Name] = structErrors
				// free up memory map no longer needed
				structErrors = nil
			}

		default:

			if fieldError := v.validateFieldByNameAndTag(valueField.Interface(), typeField.Name, tag); fieldError != nil {
				validationErrors.Errors[fieldError.Field] = fieldError
				// free up memory reference
				fieldError = nil
			}
		}
	}

	if len(validationErrors.Errors) == 0 && len(validationErrors.StructErrors) == 0 {
		return nil
	}

	return validationErrors
}
复制代码

拆解一下这里干了什么事:

  1. 通过 reflect 拿到类型,值的相关数据,常规操作,并且准备好 Error
structValue := reflect.ValueOf(s)
structType := reflect.TypeOf(s)
structName := structType.Name()

validationErrors := &StructValidationErrors{
        Struct:       structName,
        Errors:       map[string]*FieldValidationError{},
        StructErrors: map[string]*StructValidationErrors{},
}
复制代码
  1. 适配 struct 指针的场景,还是一样的处理,通过 Elem() 取值
if structValue.Kind() == reflect.Ptr && !structValue.IsNil() {
        return v.ValidateStruct(structValue.Elem().Interface())
}
复制代码
  1. 校验类型,不是 struct 或 interface 的话,不处理
if structValue.Kind() != reflect.Struct && structValue.Kind() != reflect.Interface {
        panic("interface passed for validation is not a struct")
}
复制代码
  1. 用我们前面说到的方法,从 struct 深入到每个 field,经典的写法,基于 NumField

image.png

在这个 for 循环里拿到各个 field 走老逻辑处理

  1. 针对嵌套处理很有意思,继续往下走一层
switch valueField.Kind() {

case reflect.Struct, reflect.Interface:

        if !unicode.IsUpper(rune(typeField.Name[0])) {
                continue
        }

        if structErrors := v.ValidateStruct(valueField.Interface()); structErrors != nil {
                validationErrors.StructErrors[typeField.Name] = structErrors
                // free up memory map no longer needed
                structErrors = nil
        }

default:

        if fieldError := v.validateFieldByNameAndTag(valueField.Interface(), typeField.Name, tag); fieldError != nil {
                validationErrors.Errors[fieldError.Field] = fieldError
                // free up memory reference
                fieldError = nil
        }
}
复制代码

先判断如果是非导出的,就不管了,这一点用 !unicode.IsUpper(rune(typeField.Name[0])) 做到。 然后继续调用自己,区别在于此刻传入的已经是个 field 了,这就解决了嵌套 struct 的情况,继续重新走一遍流程,一路递归。

如果已经是 field,就会走到 default 逻辑里面,还是用我们上面解析过的 validateFieldByNameAndTag 方法来处理。

源码小结

到这里我们就把初版 validator 的核心代码全都过了一遍,理解难度并不高,相信大家看两遍就能懂。

这里还是感慨,想同时保证 feature 的丰富和代码的整洁还是一件不容易的事,此刻最新的 v10 版本代码复杂度和功能远比这个版本多。但核心的,底层逻辑其实就是这么简单。

其实很多时候大家只是需要这个层级提供的能力就够了,通过 AddFunction 加上自己场景的校验函数基本可以 cover 90% 以上的场景。性能上一定也是更高的,对于不需要高阶功能的业务,大家可以按需取用。

当然,我们的解析不可能到这里就结束。之所以选取 v1 代码来介绍,就是因为保持好理解的同时,有很多有意思的点可以借鉴。下面我们梳理一下,大家可以用到日常开发中。

写法沉淀

获取 Field 的 tag 标签

还是用上面提到的这个经典例子,reflect.TypeOf 获取结构体类型,然后 NumField 遍历各个属性,拿到 reflect.StructField 之后,直接获取 Tag 变量即可。还可以用 Get 方法直接拿你想要的子标签。

大家仿照下面写就 ok:

package main

import (
	"fmt"
	"reflect"
)

type MyStruct struct {
	Location string `customTag:"custom value"`
}

func main() {
	t := reflect.TypeOf(MyStruct{})
	for i := 0; i < t.NumField(); i++ {
		field := t.Field(i)
		tag := field.Tag
		fmt.Printf("tag=%+v\n", tag)

		customTagVal := tag.Get("customTag")
		fmt.Printf("customTagVal=%s\n", customTagVal)

		lookVal, exist := tag.Lookup("customTag")
		fmt.Printf("lookVal=%s, exist=%v\n", lookVal, exist)
	}
}
复制代码

判断 interface{} 是否为 nil

func hasValue(field interface{}) bool {

	st := reflect.ValueOf(field)

	switch st.Kind() {

	case reflect.Slice, reflect.Map, reflect.Array:
		return field != nil && int64(st.Len()) > 0

	default:
		return field != nil && field != reflect.Zero(reflect.TypeOf(field)).Interface()
	}
}
复制代码

反射解指针

我们拿到一个 interface{} 变量,需要支持 struct 以及一个指向 struct 类型的指针,这时候怎么处理呢?

参考 ValidateStruct 里面对 structValue 的写法,这里 structValue 是个 reflect.Value,本质是用 Elem().Interface() 来转化。

if structValue.Kind() == reflect.Ptr && !structValue.IsNil() {
        return v.ValidateStruct(structValue.Elem().Interface())
}
复制代码

【与】【或】并行的处理

这种场景其实一点都不少见,很多时候业务也会有类似两套逻辑并存的场景,如何同时兼容?

这里就可以参考 validator 用两个 for 循环嵌套,外层是【与】,内层是【或】。大家好好品味一下这里返回值的处理,很简洁且有用。

image.png

判断 field 是否为大写(即可导出)

还是第一次见走 rune 转换,然后靠 unicode.IsUpper 来判断的写法,可以尝试一下:

image.png

默认实现 wrapper

对于大部分使用者来说,可能连自定义校验函数都用不到,这时候就可以依赖 validation 包里内置的

var internalValidator = NewValidator(defaultTagName, BakedInValidators)
复制代码

这个 Validator 来进行逻辑,事实上导出的函数底层都是调用了 internalValidator 的同名方法。

而如果你希望扩展,可以调用 NewValidator 配置你想指定的 tag 名称,以及校验函数

// NewValidator creates a new Validator instance
// NOTE: it is not necessary to create a new validator as the internal one will do in 99.9% of cases, but the option is there.
func NewValidator(tagName string, funcs map[string]ValidationFunc) *Validator {
	return &Validator{
		tagName:         tagName,
		validationFuncs: funcs,
	}
}
复制代码

而且这里的校验函数签名也很通用,大家可以根据自身诉求来扩展,整体还是能够很灵活支持各种场景的。

// ValidationFunc that accepts the value of a field and parameter for use in validation (parameter not always used or needed)
type ValidationFunc func(v interface{}, param string) bool
复制代码

结语

感谢大家阅读到这里,validator 初版的代码非常整洁和精巧,值得大家好好学习,如何用最少的代码实现一个通用校验器的功能。想看上面代码的同学可以把 validator clone 下来,然后 checkout 到这个 commit 即可:a0a1361。

当然,目前最新的 v10 版本功能丰富了很多,最好的demo其实是官方的 example 仓库,很好看懂,这里不再赘述,感兴趣的同学可以了解一下,也许你的场景就能满足。

经典开源库设计和原理解析的系列还会继续,我希望能够通过读源码,了解实现的核心逻辑,学习到一些能应用于日常开发的技巧,并和大家分享。

感谢阅读,有问题欢迎在评论区交流!

猜你喜欢

转载自juejin.im/post/7136135907249225758
今日推荐