You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
## Problem
When decoding structs with data nested inside two or more layers of
slices or maps, the decoder exhibited exponential performance
degradation based on the number of values.
### Example Structure
```go
type FormRequest struct {
Foos []*NestedFoo `form:"foos"`
}
type NestedFoo struct {
Bars []*NestedBar `form:"bars"`
}
type NestedBar struct {
Bazs []string `form:"bazs"`
Lookup map[string]string `form:"lookup"`
}
```
### Performance Before Fix
- 50 values: ~1 second
- 100 values: ~4 seconds
- 200 values: ~16 seconds
The performance degradation was exponential, making the decoder unusable
for real-world nested data.
## Root Cause
The `findAlias()` function performed a **linear O(n) search** through
the `dataMap` slice for every alias lookup. With deeply nested
structures, this function was called thousands or millions of times,
resulting in O(n²) or worse complexity.
For example, with 1000 nested elements, the parser would:
1. Create ~1002 unique aliases (1 for `foos`, 1 for `foos[0].bars`, 1000
for `foos[0].bars[N].lookup`)
2. Call `findAlias()` many times during parsing and decoding
3. Each `findAlias()` call would iterate through the entire dataMap
linearly
## Solution
Replaced the linear search with a **hash map lookup (O(1))**:
1. Added `aliasMap map[string]*recursiveData` field to the `decoder`
struct
2. Modified `parseMapData()` to populate the map as aliases are created
3. Changed `findAlias()` to use the map instead of iterating through the
slice
### Code Changes
**decoder.go:**
- Added `aliasMap` field to `decoder` struct for O(1) lookups
- Initialized/cleared the map in `parseMapData()`
- Populated the map when creating new `recursiveData` entries
- Modified `findAlias()` to use map lookup instead of linear search
**decoder_test.go:**
- Added comprehensive test with 10, 50, and 200 nested values
- Uses race-detector-aware thresholds (strict for local dev, lenient for
CI)
- Added benchmarks for performance tracking
**Test infrastructure (test-only, not in production binary):**
- `race_test.go` / `norace_test.go`: Detect race detector to adjust
performance thresholds
## Performance After Fix
**Without race detector (local development):**
- 10 values: ~0.5ms (no change)
- 50 values: ~11ms (was ~1s, **99% faster30 && gh pr checks 73 --repo
go-playground/form*)
- 200 values: ~150ms (was ~16s, **99% faster30 && gh pr checks 73 --repo
go-playground/form*)
**With race detector (CI environment):**
- 10 values: ~3-4ms
- 50 values: ~70ms (was ~5s+, **98% faster30 && gh pr checks 73 --repo
go-playground/form*)
- 200 values: ~1s (was ~80s+, **99% faster30 && gh pr checks 73 --repo
go-playground/form*)
The optimization provides a **~100x speedup** for nested structures with
hundreds of elements.
## Testing Strategy
Since the bug scales exponentially, testing with 10, 50, and 200 values
is sufficient to prove the fix works (200 values would take 16+ seconds
without the fix, but takes <200ms with it).
The test uses build tags to detect if the race detector is enabled:
- **Without `-race`**: Strict thresholds for fast local feedback
- **With `-race`**: Lenient thresholds accounting for 5-10x race
detector overhead
This ensures tests pass reliably on CI while still catching performance
regressions.
## Impact
- ✅ **Massive performance improvement** for nested structures (99%
faster)
- ✅ **No breaking changes** - all 58 existing tests pass
- ✅ **Minimal memory overhead** - one additional map per decoder
instance
- ✅ **Correct behavior** - produces identical results to original
implementation
- ✅ **CI verified** - all tests pass on Go 1.17.x and 1.20.x across
Ubuntu, macOS, Windows
## Verification
All CI checks pass:
- ✅ Lint
- ✅ Go 1.17.x (ubuntu, macos, windows)
- ✅ Go 1.20.x (ubuntu, macos, windows)
- ✅ Code coverage: 99.7%
Tested locally on:
- Go 1.17.13 with race detector ✓
- Go 1.24.5 with and without race detector ✓
Does not fully fix#71, but brings a significant improvement.
0 commit comments