Skip to content

Commit e019c28

Browse files
joeybloggsjoeybloggs
authored andcommitted
Add struct field + associated tags caching
* This essentially reduces the number of cache tag lookups for a structs fields to one.
1 parent 94182a2 commit e019c28

File tree

4 files changed

+108
-71
lines changed

4 files changed

+108
-71
lines changed

README.md

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -310,34 +310,34 @@ Benchmarks
310310
```go
311311
$ go test -cpu=4 -bench=. -benchmem=true
312312
PASS
313-
BenchmarkFieldSuccess-4 10000000 163 ns/op 0 B/op 0 allocs/op
314-
BenchmarkFieldFailure-4 2000000 673 ns/op 400 B/op 4 allocs/op
315-
BenchmarkFieldDiveSuccess-4 500000 3019 ns/op 480 B/op 27 allocs/op
316-
BenchmarkFieldDiveFailure-4 500000 3553 ns/op 880 B/op 31 allocs/op
317-
BenchmarkFieldCustomTypeSuccess-4 5000000 347 ns/op 32 B/op 2 allocs/op
318-
BenchmarkFieldCustomTypeFailure-4 2000000 645 ns/op 400 B/op 4 allocs/op
319-
BenchmarkFieldOrTagSuccess-4 1000000 1177 ns/op 16 B/op 1 allocs/op
320-
BenchmarkFieldOrTagFailure-4 1000000 1093 ns/op 432 B/op 6 allocs/op
321-
BenchmarkStructLevelValidationSuccess-4 2000000 702 ns/op 160 B/op 6 allocs/op
322-
BenchmarkStructLevelValidationFailure-4 1000000 1279 ns/op 592 B/op 11 allocs/op
323-
BenchmarkStructSimpleCustomTypeSuccess-4 1000000 1010 ns/op 80 B/op 5 allocs/op
324-
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1544 ns/op 624 B/op 11 allocs/op
325-
BenchmarkStructPartialSuccess-4 1000000 1249 ns/op 400 B/op 11 allocs/op
326-
BenchmarkStructPartialFailure-4 1000000 1797 ns/op 816 B/op 16 allocs/op
327-
BenchmarkStructExceptSuccess-4 2000000 927 ns/op 368 B/op 9 allocs/op
328-
BenchmarkStructExceptFailure-4 1000000 1259 ns/op 400 B/op 11 allocs/op
329-
BenchmarkStructSimpleCrossFieldSuccess-4 1000000 1076 ns/op 128 B/op 6 allocs/op
330-
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1623 ns/op 560 B/op 11 allocs/op
331-
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1582 ns/op 176 B/op 9 allocs/op
332-
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2139 ns/op 608 B/op 14 allocs/op
333-
BenchmarkStructSimpleSuccess-4 1000000 1040 ns/op 48 B/op 3 allocs/op
334-
BenchmarkStructSimpleFailure-4 1000000 1683 ns/op 624 B/op 11 allocs/op
335-
BenchmarkStructSimpleSuccessParallel-4 5000000 356 ns/op 48 B/op 3 allocs/op
336-
BenchmarkStructSimpleFailureParallel-4 2000000 831 ns/op 624 B/op 11 allocs/op
337-
BenchmarkStructComplexSuccess-4 200000 6738 ns/op 512 B/op 30 allocs/op
338-
BenchmarkStructComplexFailure-4 200000 11387 ns/op 3415 B/op 72 allocs/op
339-
BenchmarkStructComplexSuccessParallel-4 500000 2330 ns/op 512 B/op 30 allocs/op
340-
BenchmarkStructComplexFailureParallel-4 300000 4857 ns/op 3416 B/op 72 allocs/op
313+
BenchmarkFieldSuccess-4 10000000 162 ns/op 0 B/op 0 allocs/op
314+
BenchmarkFieldFailure-4 2000000 678 ns/op 400 B/op 4 allocs/op
315+
BenchmarkFieldDiveSuccess-4 500000 3079 ns/op 480 B/op 27 allocs/op
316+
BenchmarkFieldDiveFailure-4 300000 3584 ns/op 880 B/op 31 allocs/op
317+
BenchmarkFieldCustomTypeSuccess-4 5000000 345 ns/op 32 B/op 2 allocs/op
318+
BenchmarkFieldCustomTypeFailure-4 2000000 650 ns/op 400 B/op 4 allocs/op
319+
BenchmarkFieldOrTagSuccess-4 1000000 1188 ns/op 16 B/op 1 allocs/op
320+
BenchmarkFieldOrTagFailure-4 1000000 1088 ns/op 432 B/op 6 allocs/op
321+
BenchmarkStructLevelValidationSuccess-4 2000000 689 ns/op 160 B/op 6 allocs/op
322+
BenchmarkStructLevelValidationFailure-4 1000000 1290 ns/op 592 B/op 11 allocs/op
323+
BenchmarkStructSimpleCustomTypeSuccess-4 2000000 911 ns/op 80 B/op 5 allocs/op
324+
BenchmarkStructSimpleCustomTypeFailure-4 1000000 1446 ns/op 624 B/op 11 allocs/op
325+
BenchmarkStructPartialSuccess-4 1000000 1221 ns/op 384 B/op 10 allocs/op
326+
BenchmarkStructPartialFailure-4 1000000 1764 ns/op 800 B/op 15 allocs/op
327+
BenchmarkStructExceptSuccess-4 2000000 941 ns/op 336 B/op 7 allocs/op
328+
BenchmarkStructExceptFailure-4 1000000 1237 ns/op 384 B/op 10 allocs/op
329+
BenchmarkStructSimpleCrossFieldSuccess-4 2000000 970 ns/op 128 B/op 6 allocs/op
330+
BenchmarkStructSimpleCrossFieldFailure-4 1000000 1560 ns/op 560 B/op 11 allocs/op
331+
BenchmarkStructSimpleCrossStructCrossFieldSuccess-4 1000000 1542 ns/op 176 B/op 9 allocs/op
332+
BenchmarkStructSimpleCrossStructCrossFieldFailure-4 1000000 2147 ns/op 608 B/op 14 allocs/op
333+
BenchmarkStructSimpleSuccess-4 2000000 847 ns/op 48 B/op 3 allocs/op
334+
BenchmarkStructSimpleFailure-4 1000000 1497 ns/op 624 B/op 11 allocs/op
335+
BenchmarkStructSimpleSuccessParallel-4 5000000 257 ns/op 48 B/op 3 allocs/op
336+
BenchmarkStructSimpleFailureParallel-4 2000000 586 ns/op 624 B/op 11 allocs/op
337+
BenchmarkStructComplexSuccess-4 300000 5104 ns/op 496 B/op 29 allocs/op
338+
BenchmarkStructComplexFailure-4 200000 9840 ns/op 3400 B/op 71 allocs/op
339+
BenchmarkStructComplexSuccessParallel-4 1000000 1540 ns/op 496 B/op 29 allocs/op
340+
BenchmarkStructComplexFailureParallel-4 500000 3478 ns/op 3400 B/op 71 allocs/op
341341
```
342342

343343
How to Contribute

cache.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package validator
22

3-
import "sync"
3+
import (
4+
"reflect"
5+
"sync"
6+
)
47

58
type cachedField struct {
69
Idx int
@@ -16,23 +19,24 @@ type cachedStruct struct {
1619

1720
type structCacheMap struct {
1821
lock sync.RWMutex
19-
m map[string]*cachedStruct
22+
m map[reflect.Type]*cachedStruct
2023
}
2124

22-
func (s *structCacheMap) Get(key string) (*cachedStruct, bool) {
25+
func (s *structCacheMap) Get(key reflect.Type) (*cachedStruct, bool) {
2326
s.lock.RLock()
2427
value, ok := s.m[key]
2528
s.lock.RUnlock()
2629
return value, ok
2730
}
2831

29-
func (s *structCacheMap) Set(key string, value *cachedStruct) {
32+
func (s *structCacheMap) Set(key reflect.Type, value *cachedStruct) {
3033
s.lock.Lock()
3134
s.m[key] = value
3235
s.lock.Unlock()
3336
}
3437

3538
type cachedTag struct {
39+
tag string
3640
isOmitEmpty bool
3741
isNoStructLevel bool
3842
isStructOnly bool

util.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -247,19 +247,20 @@ func panicIf(err error) {
247247
}
248248
}
249249

250-
func (v *Validate) parseStruct(topStruct reflect.Type, sName string) *cachedStruct {
250+
func (v *Validate) parseStruct(current reflect.Value, sName string) *cachedStruct {
251251

252+
typ := current.Type()
252253
s := &cachedStruct{Name: sName, fields: map[int]cachedField{}}
253254

254-
numFields := topStruct.NumField()
255+
numFields := current.NumField()
255256

256257
var fld reflect.StructField
257258
var tag string
258259
var customName string
259260

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

262-
fld = topStruct.Field(i)
263+
fld = typ.Field(i)
263264

264265
if len(fld.PkgPath) != 0 {
265266
continue
@@ -290,14 +291,14 @@ func (v *Validate) parseStruct(topStruct reflect.Type, sName string) *cachedStru
290291
s.fields[i] = cachedField{Idx: i, Name: fld.Name, AltName: customName, CachedTag: cTag}
291292
}
292293

293-
v.structCache.Set(sName, s)
294+
v.structCache.Set(typ, s)
294295

295296
return s
296297
}
297298

298299
func (v *Validate) parseTags(tag, fieldName string) *cachedTag {
299300

300-
cTag := &cachedTag{}
301+
cTag := &cachedTag{tag: tag}
301302

302303
v.parseTagsRecursive(cTag, tag, fieldName, blank, false)
303304

validator.go

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"strings"
1717
"sync"
1818
"time"
19-
"unicode"
2019
)
2120

2221
const (
@@ -196,7 +195,7 @@ func New(config *Config) *Validate {
196195
tagName: config.TagName,
197196
fieldNameTag: config.FieldNameTag,
198197
tagCache: &tagCacheMap{m: map[string]*cachedTag{}},
199-
structCache: &structCacheMap{m: map[string]*cachedStruct{}},
198+
structCache: &structCacheMap{m: map[reflect.Type]*cachedStruct{}},
200199
errsPool: &sync.Pool{New: func() interface{} {
201200
return ValidationErrors{}
202201
}}}
@@ -307,7 +306,7 @@ func (v *Validate) Field(field interface{}, tag string) error {
307306
errs := v.errsPool.Get().(ValidationErrors)
308307
fieldVal := reflect.ValueOf(field)
309308

310-
v.traverseField(fieldVal, fieldVal, fieldVal, blank, errs, false, tag, blank, blank, false, false, nil)
309+
v.traverseField(fieldVal, fieldVal, fieldVal, blank, errs, false, tag, blank, blank, false, false, nil, nil)
311310

312311
if len(errs) == 0 {
313312
v.errsPool.Put(errs)
@@ -327,7 +326,7 @@ func (v *Validate) FieldWithValue(val interface{}, field interface{}, tag string
327326
errs := v.errsPool.Get().(ValidationErrors)
328327
topVal := reflect.ValueOf(val)
329328

330-
v.traverseField(topVal, topVal, reflect.ValueOf(field), blank, errs, false, tag, blank, blank, false, false, nil)
329+
v.traverseField(topVal, topVal, reflect.ValueOf(field), blank, errs, false, tag, blank, blank, false, false, nil, nil)
331330

332331
if len(errs) == 0 {
333332
v.errsPool.Put(errs)
@@ -452,50 +451,80 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
452451
panic("value passed for validation is not a struct")
453452
}
454453

455-
var ok bool
454+
// var ok bool
456455
typ := current.Type()
457456

457+
sName := typ.Name()
458+
458459
if useStructName {
459-
errPrefix += typ.Name() + "."
460+
errPrefix += sName + "."
460461
}
461462

462463
// structonly tag present don't tranverseFields
463464
// but must still check and run below struct level validation
464465
// if present
465466
if !isStructOnly {
466-
numFields := current.NumField()
467467

468468
var fld reflect.StructField
469-
var customName string
470469

471-
for i := 0; i < numFields; i++ {
472-
fld = typ.Field(i)
470+
// is anonymous struct, cannot parse or cache as
471+
// it has no name to index by
472+
if len(sName) == 0 {
473473

474-
if !unicode.IsUpper(rune(fld.Name[0])) {
475-
continue
476-
}
474+
var customName string
475+
var ok bool
476+
numFields := current.NumField()
477477

478-
if partial {
478+
for i := 0; i < numFields; i++ {
479479

480-
_, ok = includeExclude[errPrefix+fld.Name]
480+
fld = typ.Field(i)
481481

482-
if (ok && exclude) || (!ok && !exclude) {
482+
if len(fld.PkgPath) != 0 {
483483
continue
484484
}
485-
}
486485

487-
customName = fld.Name
488-
if v.fieldNameTag != "" {
486+
if partial {
489487

490-
name := strings.SplitN(fld.Tag.Get(v.fieldNameTag), ",", 2)[0]
488+
_, ok = includeExclude[errPrefix+fld.Name]
491489

492-
// dash check is for json "-" means don't output in json
493-
if name != "" && name != "-" {
494-
customName = name
490+
if (ok && exclude) || (!ok && !exclude) {
491+
continue
492+
}
495493
}
494+
495+
customName = fld.Name
496+
if v.fieldNameTag != "" {
497+
498+
name := strings.SplitN(fld.Tag.Get(v.fieldNameTag), ",", 2)[0]
499+
500+
// dash check is for json "-" means don't output in json
501+
if name != "" && name != "-" {
502+
customName = name
503+
}
504+
}
505+
506+
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude, nil)
496507
}
508+
} else {
509+
s, ok := v.structCache.Get(typ)
510+
if !ok {
511+
s = v.parseStruct(current, sName)
512+
}
513+
514+
for i, f := range s.fields {
515+
516+
if partial {
517+
518+
_, ok = includeExclude[errPrefix+f.Name]
497519

498-
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, fld.Tag.Get(v.tagName), fld.Name, customName, partial, exclude, includeExclude)
520+
if (ok && exclude) || (!ok && !exclude) {
521+
continue
522+
}
523+
}
524+
fld = typ.Field(i)
525+
526+
v.traverseField(topStruct, currentStruct, current.Field(i), errPrefix, errs, true, f.CachedTag.tag, fld.Name, f.AltName, partial, exclude, includeExclude, f.CachedTag)
527+
}
499528
}
500529
}
501530

@@ -508,16 +537,19 @@ func (v *Validate) tranverseStruct(topStruct reflect.Value, currentStruct reflec
508537
}
509538

510539
// 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
511-
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{}) {
540+
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) {
512541

513542
if tag == skipValidationTag {
514543
return
515544
}
516545

517-
cTag, isCached := v.tagCache.Get(tag)
546+
if cTag == nil {
547+
var isCached bool
548+
cTag, isCached = v.tagCache.Get(tag)
518549

519-
if !isCached {
520-
cTag = v.parseTags(tag, name)
550+
if !isCached {
551+
cTag = v.parseTags(tag, name)
552+
}
521553
}
522554

523555
current, kind := v.ExtractType(current)
@@ -615,9 +647,9 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.
615647
// or panic ;)
616648
switch kind {
617649
case reflect.Slice, reflect.Array:
618-
v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude)
650+
v.traverseSlice(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude, nil)
619651
case reflect.Map:
620-
v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude)
652+
v.traverseMap(topStruct, currentStruct, current, errPrefix, errs, diveSubTag, name, customName, partial, exclude, includeExclude, nil)
621653
default:
622654
// throw error, if not a slice or map then should not have gotten here
623655
// bad dive tag
@@ -627,18 +659,18 @@ func (v *Validate) traverseField(topStruct reflect.Value, currentStruct reflect.
627659
}
628660

629661
// traverseSlice traverses a Slice or Array's elements and passes them to traverseField for validation
630-
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{}) {
662+
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) {
631663

632664
for i := 0; i < current.Len(); i++ {
633-
v.traverseField(topStruct, currentStruct, current.Index(i), errPrefix, errs, false, tag, fmt.Sprintf(arrayIndexFieldName, name, i), fmt.Sprintf(arrayIndexFieldName, customName, i), partial, exclude, includeExclude)
665+
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)
634666
}
635667
}
636668

637669
// traverseMap traverses a map's elements and passes them to traverseField for validation
638-
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{}) {
670+
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) {
639671

640672
for _, key := range current.MapKeys() {
641-
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)
673+
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)
642674
}
643675
}
644676

0 commit comments

Comments
 (0)