echo 源码分析之校验器:Validator
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 mainimport ("sync""github.com/go-playground/validator/v10")type CustomValidator struct {once sync.Oncevalidate *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 mainimport ("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_emailEmail 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包含哪些文件
% lsLICENSEMakefileREADME.md_examplesnon-standardtestdatatranslationsbaked_in.gobenchmarks_test.gocache.gocountry_codes.godoc.goerrors.gofield_level.gogo.modgo.sumlogo.pngregexes.gostruct_level.gotranslations.goutil.govalidator.govalidator_instance.govalidator_test.go% ls non-standard/validators/notblank.gonotblank_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.govalidator_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 instancefor k, val := range bakedInAliases {v.RegisterAlias(k, val)}// must copy validators for separate validations to be used in each instancefor k, val := range bakedInValidators {switch k {// these require that even if the value is nil that the validation should run, omitempty still overrides this behaviourcase 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 //对应的tagTypehasTag 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 //结构体对应的字段mapfn 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 structtype 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 *Validatetop reflect.Valuens []byteactualNs []byteerrs ValidationErrorsincludeExclude map[string]struct{} // reset only if StructPartial or StructExcept are called, no need otherwiseffn FilterFuncslflParent reflect.Value // StructLevel & FieldLevelslCurrent reflect.Value // StructLevel & FieldLevelflField reflect.Value // StructLevel & FieldLevelcf *cField // StructLevel & FieldLevelct *cTag // StructLevel & FieldLevelmisc []byte // misc reusablestr1 string // misc reusablestr2 string // misc reusablefldIsPointer bool // StructLevel & FieldLevelisPartial boolhasExcludes 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.Mutexm 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] = valuesc.m.Store(nm)}
2.tagCache(标签规则缓存)
这个缓存存储的就是 【tag】 ->【cTag】之间的对应关系,考虑并发的问题,这边是进行加锁存储:
type tagCache struct {lock sync.Mutexm 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] = valuetc.m.Store(nm)}

推荐阅读
