echo 源码分析之校验器:Validator
共 10019字,需浏览 21分钟
·
2021-05-10 17:57
echo 默认没有自己的validator,只提供了接口,需要自己实现
Echo struct {
Validator Validator
}
validator需要实现Validate接口
Validator interface {
Validate(i interface{}) error
}
所以我们可以包装一下go-playground/validator来实现echo的validator
由于go-playground/validator并没有实现Validate方法,所以不能直接赋值
e.Validator := validator.New()
如何实现呢,可以自定义一个接口,中间调用validate.Struct(i)方法
package main
import (
"sync"
"github.com/go-playground/validator/v10"
)
type CustomValidator struct {
once sync.Once
validate *validator.Validate
}
func (c *CustomValidator) Validate(i interface{}) error {
c.lazyInit()
return c.validate.Struct(i)
}
func (c *CustomValidator) lazyInit() {
c.once.Do(func() {
c.validate = validator.New()
})
}
func NewCustomValidator() *CustomValidator {
return &CustomValidator{}
}
接着就可以赋值给echo的validator了
package main
import (
"fmt"
"net/http"
"github.com/labstack/echo/v4"
)
type User struct {
Name string `json:"name" param:"name" query:"name" form:"name" xml:"name" validate:"required"` // //curl -XGET http://localhost:1323/users/Joe\?email\=joe_email
Email string `json:"email" form:"email" query:"email"`
}
func main() {
e := echo.New()
e.Validator = NewCustomValidator()
e.GET("/users/:name", func(c echo.Context) error {
u := new(User)
u.Name = c.Param("name")
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, nil)
}
if err := c.Validate(u); err != nil {
return c.JSON(http.StatusBadRequest, nil)
}
return c.JSON(http.StatusOK, u)
})
fmt.Println(e.Start(":1336"))
}
我们看下go-playground/validator包含哪些文件
% ls
LICENSE
Makefile
README.md
_examples
non-standard
testdata
translations
baked_in.go
benchmarks_test.go
cache.go
country_codes.go
doc.go
errors.go
field_level.go
go.mod
go.sum
logo.png
regexes.go
struct_level.go
translations.go
util.go
validator.go
validator_instance.go
validator_test.go
% ls non-standard/validators/
notblank.go
notblank_test.go
主要是下面几个部分:
baked_in.go :定义默认【标签校验器】和【别名校验器】,程序初始的时候直接赋值了默认的校验器,相当于你买了个机器人送几根电池的行为。当然这边你的校验器可以手动添加自定义,后面会说到
cache.go:定义结构体校验器缓存、字段校验器缓存和获取的方法,一个validator对象如果一直存活,他会把之前处理过的结构体或者字段校验器进行缓存.
regexes.go:【标签校验器】里面有一些使用到正则进行校验的,这边存储的就是静态的正则表达式
util.go:工具类,一般是用在【标签校验器】里面进行处理
validator.go:校验类主体,提供四个主要的校验方法
validator一共提供了四种校验器:
validationFuncs map[string]Func //规则类型的校验 【tag标签】-> 校验规则
structLevelFuncs map[reflect.Type]StructLevelFunc //规则结构体的校验 【结构体类型】-> 校验规则
customTypeFuncs map[reflect.Type]CustomTypeFunc //类型校验器 【数据类型】-> 校验规则
aliasValidators map[string]string //别名校验器 【别名匹配规则组合】-> 校验规则
其中比较重要的就是
validator.go
validator_instance.go
这两个文件,在validator_instance.go中的方法是公有的
func New() *Validate {
tc := new(tagCache)
tc.m.Store(make(map[string]*cTag))
sc := new(structCache)
sc.m.Store(make(map[reflect.Type]*cStruct))
v := &Validate{
tagName: defaultTagName,
aliases: make(map[string]string, len(bakedInAliases)),
validations: make(map[string]internalValidationFuncWrapper, len(bakedInValidators)),
tagCache: tc,
structCache: sc,
}
// must copy alias validators for separate validations to be used in each validator instance
for k, val := range bakedInAliases {
v.RegisterAlias(k, val)
}
// must copy validators for separate validations to be used in each instance
for k, val := range bakedInValidators {
switch k {
// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviour
case requiredIfTag, requiredUnlessTag, requiredWithTag, requiredWithAllTag, requiredWithoutTag, requiredWithoutAllTag:
_ = v.registerValidation(k, wrapFunc(val), true, true)
default:
// no need to error check here, baked in will always be valid
_ = v.registerValidation(k, wrapFunc(val), true, false)
}
}
v.pool = &sync.Pool{
New: func() interface{} {
return &validate{
v: v,
ns: make([]byte, 0, 64),
actualNs: make([]byte, 0, 64),
misc: make([]byte, 32),
}
},
}
return v
}
由于validate是每一个请求都需要的高频操作,所以非常关注性能,尽量使用缓存。
校验器结构体
Ⅰ.cTag(tag规则)
cTag是一个链表,存储一连串的相关联tag的校验器,比如说这边是作为存储一个Field的相关所有标签,看一下cTag的结构:
type cTag struct {
tag string //标签
aliasTag string //
actualAliasTag string //
param string //如果是比较类型的标签,这里存放的是比较的值,比如说 min=10,这里存放的是【10】这个值
hasAlias bool //是否有别名校验器标签
typeof tagType //对应的tagType
hasTag bool //是否存在tag标签
fn Func //当前cTag对应的【tag标签校验器】
next *cTag //下一个cTag标签
}
Ⅱ.cFeild(字段规则)
cField代表一个结构体字段对应的规则,他会包含这个结构体字段对应的所有tag规则,也就是一组cTag链表存储的规则:
type cField struct {
Idx int //字段下标
Name string //字段名
AltName string //
cTags *cTag //Field对应的cTag规则,是一个链表(一串的规则).
}
Ⅲ.cStruct(结构体规则)
同理,cStruct对应的是这个结构体所有字段(包含tag规则),以及这个结构体自己对应的【StructLevelFunc】的校验方法:
type cStruct struct {
Name string //结构体名称
fields map[int]*cField //结构体对应的字段map
fn StructLevelFunc //结构体校验器 (结构体类型->结构体校验器)
}
可以看下调用Struct来进行验证的过程
func (v *Validate) Struct(s interface{}) error {
return v.StructCtx(context.Background(), s)
}
func (v *Validate) StructCtx(ctx context.Context, s interface{}) (err error) {
vd := v.pool.Get().(*validate)
vd.validateStruct(ctx, top, val, val.Type(), vd.ns[0:0], vd.actualNs[0:0], nil)
}
其中 validate是校验类的主体,所有的注册和缓存数据、错误信息数据都是存储在validate中的,看一下具体的数据结构:
// Validate contains the validator settings passed in using the Config struct
type Validate struct {
tagName string //校验起作用的tag名
fieldNameTag string //
validationFuncs map[string]Func //规则类型的校验 【tag标签】-> 校验规则
structLevelFuncs map[reflect.Type]StructLevelFunc //规则结构体的校验 【结构体类型】-> 校验规则
customTypeFuncs map[reflect.Type]CustomTypeFunc //类型校验器 【数据类型】-> 校验规则
aliasValidators map[string]string //别名校验器 【别名匹配规则组合】-> 校验规则
hasCustomFuncs bool //是否存在类型校验器
hasAliasValidators bool //是否有别名校验器
hasStructLevelFuncs bool //是否有结构体校验器
tagCache *tagCache //tag对应的【校验规则方法】的缓存
structCache *structCache //结构体对应的【校验规则方法】的缓存
errsPool *sync.Pool //校验错误奖池
}
定义在validator_instance.go
type validate struct {
v *Validate
top reflect.Value
ns []byte
actualNs []byte
errs ValidationErrors
includeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwise
ffn FilterFunc
slflParent reflect.Value // StructLevel & FieldLevel
slCurrent reflect.Value // StructLevel & FieldLevel
flField reflect.Value // StructLevel & FieldLevel
cf *cField // StructLevel & FieldLevel
ct *cTag // StructLevel & FieldLevel
misc []byte // misc reusable
str1 string // misc reusable
str2 string // misc reusable
fldIsPointer bool // StructLevel & FieldLevel
isPartial bool
hasExcludes bool
}
定义在validator.go
在validator.go中还有两个方法
func (v *validate) validateStruct(ctx context.Context, parent reflect.Value, current reflect.Value, typ reflect.Type, ns []byte, structNs []byte, ct *cTag) {
}
func (v *validate) traverseField(ctx context.Context, parent reflect.Value, current reflect.Value, ns []byte, structNs []byte, cf *cField, ct *cTag) {
}
1,获取结构体校验器缓存,如果没有获取到,则对该结构体进行解析,然后返回对应的校验器,否则往下
2,判断是否存在校验器,否则忽略该字段校验(这边有一个判断是first的判断,如果是第一层,也就是传进来的机构体一定会对它的Field进行校验)
2,循环所有的Field,判断Field是否包含在不校验的集合中,如果不包含则进行校验,包含则不校验(这边通过传入一个【includeExclude】的map结构可以指定对哪些字段不进行校验.这个在【StructExcept】方法中会用到)
3,判断是否存在对应的结构体类型校验方法,如果存在则调用该方法进行校验
整个验证的过程就是利用反射和struct tag中定义的一些语法扩展,对参数的值进行校验。
在很多工具类里面对于可能多次出现的东西都会进行相应的缓存处理,这边也不例外,对于一个Validator。它可能会进行多次校验,那么可能会有重复的结构体或者字段数据,可以进行缓存不需要下次再提取,所以这边提供了两个对应的缓存。
1.structCache(结构体缓存)
这个缓存存储的就是 【结构体类型】->【cStruct】之间的对应关系,考虑并发的问题,这边是进行加锁存储:
type structCache struct {
lock sync.Mutex
m atomic.Value // map[reflect.Type]*cStruct
}
对应的缓存方法包含 Get、Set:
func (sc *structCache) Get(key reflect.Type) (c *cStruct, found bool) {
c, found = sc.m.Load().(map[reflect.Type]*cStruct)[key]
return
}
func (sc *structCache) Set(key reflect.Type, value *cStruct) {
m := sc.m.Load().(map[reflect.Type]*cStruct)
nm := make(map[reflect.Type]*cStruct, len(m)+1)
for k, v := range m {
nm[k] = v
}
nm[key] = value
sc.m.Store(nm)
}
2.tagCache(标签规则缓存)
这个缓存存储的就是 【tag】 ->【cTag】之间的对应关系,考虑并发的问题,这边是进行加锁存储:
type tagCache struct {
lock sync.Mutex
m atomic.Value // map[string]*cTag
}
对应的缓存方法包含 Get、Set:
func (tc *tagCache) Get(key string) (c *cTag, found bool) {
c, found = tc.m.Load().(map[string]*cTag)[key]
return
}
func (tc *tagCache) Set(key string, value *cTag) {
m := tc.m.Load().(map[string]*cTag)
nm := make(map[string]*cTag, len(m)+1)
for k, v := range m {
nm[k] = v
}
nm[key] = value
tc.m.Store(nm)
}
推荐阅读