Add struct field + associated tags caching

* This essentially reduces the number of cache tag lookups for a structs fields to one.
pull/217/head
joeybloggs 9 years ago
parent 94182a2199
commit e019c28542
  1. 56
      README.md
  2. 12
      cache.go
  3. 11
      util.go
  4. 66
      validator.go

@ -310,34 +310,34 @@ Benchmarks
```go ```go
$ go test -cpu=4 -bench=. -benchmem=true $ go test -cpu=4 -bench=. -benchmem=true
PASS PASS
BenchmarkFieldSuccess-4 10000000 163 ns/op 0 B/op 0 allocs/op BenchmarkFieldSuccess-4 10000000 162 ns/op 0 B/op 0 allocs/op
BenchmarkFieldFailure-4 2000000 673 ns/op 400 B/op 4 allocs/op BenchmarkFieldFailure-4 2000000 678 ns/op 400 B/op 4 allocs/op
BenchmarkFieldDiveSuccess-4 500000 3019 ns/op 480 B/op 27 allocs/op BenchmarkFieldDiveSuccess-4 500000 3079 ns/op 480 B/op 27 allocs/op
BenchmarkFieldDiveFailure-4 500000 3553 ns/op 880 B/op 31 allocs/op BenchmarkFieldDiveFailure-4 300000 3584 ns/op 880 B/op 31 allocs/op
BenchmarkFieldCustomTypeSuccess-4 5000000 347 ns/op 32 B/op 2 allocs/op BenchmarkFieldCustomTypeSuccess-4 5000000 345 ns/op 32 B/op 2 allocs/op
BenchmarkFieldCustomTypeFailure-4 2000000 645 ns/op 400 B/op 4 allocs/op BenchmarkFieldCustomTypeFailure-4 2000000 650 ns/op 400 B/op 4 allocs/op
BenchmarkFieldOrTagSuccess-4 1000000 1177 ns/op 16 B/op 1 allocs/op BenchmarkFieldOrTagSuccess-4 1000000 1188 ns/op 16 B/op 1 allocs/op
BenchmarkFieldOrTagFailure-4 1000000 1093 ns/op 432 B/op 6 allocs/op BenchmarkFieldOrTagFailure-4 1000000 1088 ns/op 432 B/op 6 allocs/op
BenchmarkStructLevelValidationSuccess-4 2000000 702 ns/op 160 B/op 6 allocs/op BenchmarkStructLevelValidationSuccess-4 2000000 689 ns/op 160 B/op 6 allocs/op
BenchmarkStructLevelValidationFailure-4 1000000 1279 ns/op 592 B/op 11 allocs/op BenchmarkStructLevelValidationFailure-4 1000000 1290 ns/op 592 B/op 11 allocs/op
BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1010 ns/op 80 B/op 5 allocs/op BenchmarkStructSimpleCustomTypeSuccess-4 2000000 911 ns/op 80 B/op 5 allocs/op
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1544 ns/op 624 B/op 11 allocs/op BenchmarkStructSimpleCustomTypeFailure-4 1000000 1446 ns/op 624 B/op 11 allocs/op
BenchmarkStructPartialSuccess-4 1000000 1249 ns/op 400 B/op 11 allocs/op BenchmarkStructPartialSuccess-4 1000000 1221 ns/op 384 B/op 10 allocs/op
BenchmarkStructPartialFailure-4 1000000 1797 ns/op 816 B/op 16 allocs/op BenchmarkStructPartialFailure-4 1000000 1764 ns/op 800 B/op 15 allocs/op
BenchmarkStructExceptSuccess-4 2000000 927 ns/op 368 B/op 9 allocs/op BenchmarkStructExceptSuccess-4 2000000 941 ns/op 336 B/op 7 allocs/op
BenchmarkStructExceptFailure-4 1000000 1259 ns/op 400 B/op 11 allocs/op BenchmarkStructExceptFailure-4 1000000 1237 ns/op 384 B/op 10 allocs/op
BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1076 ns/op 128 B/op 6 allocs/op BenchmarkStructSimpleCrossFieldSuccess-4 2000000 970 ns/op 128 B/op 6 allocs/op
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1623 ns/op 560 B/op 11 allocs/op BenchmarkStructSimpleCrossFieldFailure-4 1000000 1560 ns/op 560 B/op 11 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1582 ns/op 176 B/op 9 allocs/op BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1542 ns/op 176 B/op 9 allocs/op
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2139 ns/op 608 B/op 14 allocs/op BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2147 ns/op 608 B/op 14 allocs/op
BenchmarkStructSimpleSuccess-4 1000000 1040 ns/op 48 B/op 3 allocs/op BenchmarkStructSimpleSuccess-4 2000000 847 ns/op 48 B/op 3 allocs/op
BenchmarkStructSimpleFailure-4 1000000 1683 ns/op 624 B/op 11 allocs/op BenchmarkStructSimpleFailure-4 1000000 1497 ns/op 624 B/op 11 allocs/op
BenchmarkStructSimpleSuccessParallel-4 5000000 356 ns/op 48 B/op 3 allocs/op BenchmarkStructSimpleSuccessParallel-4 5000000 257 ns/op 48 B/op 3 allocs/op
BenchmarkStructSimpleFailureParallel-4 2000000 831 ns/op 624 B/op 11 allocs/op BenchmarkStructSimpleFailureParallel-4 2000000 586 ns/op 624 B/op 11 allocs/op
BenchmarkStructComplexSuccess-4 200000 6738 ns/op 512 B/op 30 allocs/op BenchmarkStructComplexSuccess-4 300000 5104 ns/op 496 B/op 29 allocs/op
BenchmarkStructComplexFailure-4 200000 11387 ns/op 3415 B/op 72 allocs/op BenchmarkStructComplexFailure-4 200000 9840 ns/op 3400 B/op 71 allocs/op
BenchmarkStructComplexSuccessParallel-4 500000 2330 ns/op 512 B/op 30 allocs/op BenchmarkStructComplexSuccessParallel-4 1000000 1540 ns/op 496 B/op 29 allocs/op
BenchmarkStructComplexFailureParallel-4 300000 4857 ns/op 3416 B/op 72 allocs/op BenchmarkStructComplexFailureParallel-4 500000 3478 ns/op 3400 B/op 71 allocs/op
``` ```
How to Contribute How to Contribute

@ -1,6 +1,9 @@
package validator package validator
import "sync" import (
"reflect"
"sync"
)
type cachedField struct { type cachedField struct {
Idx int Idx int
@ -16,23 +19,24 @@ type cachedStruct struct {
type structCacheMap struct { type structCacheMap struct {
lock sync.RWMutex lock sync.RWMutex
m map[string]*cachedStruct m map[reflect.Type]*cachedStruct
} }
func (s *structCacheMap) Get(key string) (*cachedStruct, bool) { func (s *structCacheMap) Get(key reflect.Type) (*cachedStruct, bool) {
s.lock.RLock() s.lock.RLock()
value, ok := s.m[key] value, ok := s.m[key]
s.lock.RUnlock() s.lock.RUnlock()
return value, ok return value, ok
} }
func (s *structCacheMap) Set(key string, value *cachedStruct) { func (s *structCacheMap) Set(key reflect.Type, value *cachedStruct) {
s.lock.Lock() s.lock.Lock()
s.m[key] = value s.m[key] = value
s.lock.Unlock() s.lock.Unlock()
} }
type cachedTag struct { type cachedTag struct {
tag string
isOmitEmpty bool isOmitEmpty bool
isNoStructLevel bool isNoStructLevel bool
isStructOnly bool isStructOnly bool

@ -247,11 +247,12 @@ func panicIf(err error) {
} }
} }
func (v *Validate) parseStruct(topStruct reflect.Type, sName string) *cachedStruct { func (v *Validate) parseStruct(current reflect.Value, sName string) *cachedStruct {
typ := current.Type()
s := &cachedStruct{Name: sName, fields: map[int]cachedField{}} s := &cachedStruct{Name: sName, fields: map[int]cachedField{}}
numFields := topStruct.NumField() numFields := current.NumField()
var fld reflect.StructField var fld reflect.StructField
var tag string var tag string
@ -259,7 +260,7 @@ func (v *Validate) parseStruct(topStruct reflect.Type, sName string) *cachedStru
for i := 0; i < numFields; i++ { for i := 0; i < numFields; i++ {
fld = topStruct.Field(i) fld = typ.Field(i)
if len(fld.PkgPath) != 0 { if len(fld.PkgPath) != 0 {
continue continue
@ -290,14 +291,14 @@ func (v *Validate) parseStruct(topStruct reflect.Type, sName string) *cachedStru
s.fields[i] = cachedField{Idx: i, Name: fld.Name, AltName: customName, CachedTag: cTag} s.fields[i] = cachedField{Idx: i, Name: fld.Name, AltName: customName, CachedTag: cTag}
} }
v.structCache.Set(sName, s) v.structCache.Set(typ, s)
return s return s
} }
func (v *Validate) parseTags(tag, fieldName string) *cachedTag { func (v *Validate) parseTags(tag, fieldName string) *cachedTag {
cTag := &cachedTag{} cTag := &cachedTag{tag: tag}
v.parseTagsRecursive(cTag, tag, fieldName, blank, false) v.parseTagsRecursive(cTag, tag, fieldName, blank, false)

@ -16,7 +16,6 @@ import (
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode"
) )
const ( const (
@ -196,7 +195,7 @@ func New(config *Config) *Validate {
tagName: config.TagName, tagName: config.TagName,
fieldNameTag: config.FieldNameTag, fieldNameTag: config.FieldNameTag,
tagCache: &tagCacheMap{m: map[string]*cachedTag{}}, tagCache: &tagCacheMap{m: map[string]*cachedTag{}},
structCache: &structCacheMap{m: map[string]*cachedStruct{}}, structCache: &structCacheMap{m: map[reflect.Type]*cachedStruct{}},
errsPool: &sync.Pool{New: func() interface{} { errsPool: &sync.Pool{New: func() interface{} {
return ValidationErrors{} return ValidationErrors{}
}}} }}}
@ -307,7 +306,7 @@ func (v *Validate) Field(field interface{}, tag string) error {
errs := v.errsPool.Get().(ValidationErrors) errs := v.errsPool.Get().(ValidationErrors)
fieldVal := reflect.ValueOf(field) fieldVal := reflect.ValueOf(field)
v.traverseField(fieldVal, fieldVal, fieldVal, blank, errs, false, tag, blank, blank, false, false, nil) v.traverseField(fieldVal, fieldVal, fieldVal, blank, errs, false, tag, blank, blank, false, false, nil, nil)
if len(errs) == 0 { if len(errs) == 0 {
v.errsPool.Put(errs) v.errsPool.Put(errs)
@ -327,7 +326,7 @@ func (v *Validate) FieldWithValue(val interface{}, field interface{}, tag string
errs := v.errsPool.Get().(ValidationErrors) errs := v.errsPool.Get().(ValidationErrors)
topVal := reflect.ValueOf(val) topVal := reflect.ValueOf(val)
v.traverseField(topVal, topVal, reflect.ValueOf(field), blank, errs, false, tag, blank, blank, false, false, nil) v.traverseField(topVal, topVal, reflect.ValueOf(field), blank, errs, false, tag, blank, blank, false, false, nil, nil)
if len(errs) == 0 { if len(errs) == 0 {
v.errsPool.Put(errs) v.errsPool.Put(errs)
@ -452,26 +451,35 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
panic("value passed for validation is not a struct") panic("value passed for validation is not a struct")
} }
var ok bool // var ok bool
typ := current.Type() typ := current.Type()
sName := typ.Name()
if useStructName { if useStructName {
errPrefix += typ.Name() + "." errPrefix += sName + "."
} }
// structonly tag present don't tranverseFields // structonly tag present don't tranverseFields
// but must still check and run below struct level validation // but must still check and run below struct level validation
// if present // if present
if !isStructOnly { if !isStructOnly {
numFields := current.NumField()
var fld reflect.StructField var fld reflect.StructField
// is anonymous struct, cannot parse or cache as
// it has no name to index by
if len(sName) == 0 {
var customName string var customName string
var ok bool
numFields := current.NumField()
for i := 0; i < numFields; i++ { for i := 0; i < numFields; i++ {
fld = typ.Field(i) fld = typ.Field(i)
if !unicode.IsUpper(rune(fld.Name[0])) { if len(fld.PkgPath) != 0 {
continue continue
} }
@ -495,7 +503,28 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
} }
} }
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude) v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude, nil)
}
} else {
s, ok := v.structCache.Get(typ)
if !ok {
s = v.parseStruct(current, sName)
}
for i, f := range s.fields {
if partial {
_, ok = includeExclude[errPrefix+f.Name]
if (ok && exclude) || (!ok && !exclude) {
continue
}
}
fld = typ.Field(i)
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, f.CachedTag.tag, fld.Name, f.AltName, partial, exclude, includeExclude, f.CachedTag)
}
} }
} }
@ -508,17 +537,20 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
} }
// traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options // traverseField validates any field, be it a struct or single field, ensures it's validity and passes it along to be validated via it's tag options
func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, isStructField bool, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}) { func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, isStructField bool, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}, cTag *cachedTag) {
if tag == skipValidationTag { if tag == skipValidationTag {
return return
} }
cTag, isCached := v.tagCache.Get(tag) if cTag == nil {
var isCached bool
cTag, isCached = v.tagCache.Get(tag)
if !isCached { if !isCached {
cTag = v.parseTags(tag, name) cTag = v.parseTags(tag, name)
} }
}
current, kind := v.ExtractType(current) current, kind := v.ExtractType(current)
var typ reflect.Type var typ reflect.Type
@ -615,9 +647,9 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.
// or panic ;) // or panic ;)
switch kind { switch kind {
case reflect.Slice, reflect.Array: case reflect.Slice, reflect.Array:
v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude) v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude, nil)
case reflect.Map: case reflect.Map:
v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude) v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude, nil)
default: default:
// throw error, if not a slice or map then should not have gotten here // throw error, if not a slice or map then should not have gotten here
// bad dive tag // bad dive tag
@ -627,18 +659,18 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.
} }
// traverseSlice traverses a Slice or Array's elements and passes them to traverseField for validation // traverseSlice traverses a Slice or Array's elements and passes them to traverseField for validation
func (v *Validate) traverseSlice(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}) { func (v *Validate) traverseSlice(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}, cTag *cachedTag) {
for i := 0; i < current.Len(); i++ { for i := 0; i < current.Len(); i++ {
v.traverseField(topStruct, currentStruct, current.Index(i), errPrefix, errs, false, tag, fmt.Sprintf(arrayIndexFieldName, name, i), fmt.Sprintf(arrayIndexFieldName, customName, i), partial, exclude, includeExclude) v.traverseField(topStruct, currentStruct, current.Index(i), errPrefix, errs, false, tag, fmt.Sprintf(arrayIndexFieldName, name, i), fmt.Sprintf(arrayIndexFieldName, customName, i), partial, exclude, includeExclude, cTag)
} }
} }
// traverseMap traverses a map's elements and passes them to traverseField for validation // traverseMap traverses a map's elements and passes them to traverseField for validation
func (v *Validate) traverseMap(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}) { func (v *Validate) traverseMap(topStruct reflect.Value, currentStruct reflect.Value, current reflect.Value, errPrefix string, errs ValidationErrors, tag, name, customName string, partial bool, exclude bool, includeExclude map[string]*struct{}, cTag *cachedTag) {
for _, key := range current.MapKeys() { for _, key := range current.MapKeys() {
v.traverseField(topStruct, currentStruct, current.MapIndex(key), errPrefix, errs, false, tag, fmt.Sprintf(mapIndexFieldName, name, key.Interface()), fmt.Sprintf(mapIndexFieldName, customName, key.Interface()), partial, exclude, includeExclude) v.traverseField(topStruct, currentStruct, current.MapIndex(key), errPrefix, errs, false, tag, fmt.Sprintf(mapIndexFieldName, name, key.Interface()), fmt.Sprintf(mapIndexFieldName, customName, key.Interface()), partial, exclude, includeExclude, cTag)
} }
} }

Loading…
Cancel
Save