Go — Flaky Tests from Random Map Iteration Order
Eliminate flaky Go tests caused by randomized map iteration order, detect the class with -count and shuffle runs, and sort keys where order matters.
Symptoms
- A test fails intermittently with identical code and inputs, passing on re-run
- Failures cluster around assertions comparing slices built from map ranges
- go test -count=50 reproduces failures at a stable rate instead of never or always
Possible causes
- Go deliberately randomizes map iteration order, so range over a map yields a different sequence per run
- Output assembled from a map range (joined keys, serialized JSON, generated IDs) differs run to run
- A test compares an ordered expectation against an unordered construction
- A chain of such outputs feeds another assertion, amplifying one difference into a deep mismatch
Diagnosis steps
Quantify the flake rate
go test -count=50 -run TestBuildOrder ./gen/ | tail -3Interpretation
A stable failure rate across many runs (say 3 in 50) is the signature of randomization, not of timing races which trend toward load dependence.
Find map-fed ordered output
grep -Rn "for .* range .*map\|for k := range" src/ | head; grep -Rn "strings.Join\|Marshal" src/gen/ | headInterpretation
A range over a map feeding a Join or Marshal call is the exact construction whose output order is run-dependent.
Make the order deterministic and re-run
go test -count=50 ./gen/Interpretation
Fifty consecutive passes after sorting keys confirm the diagnosis and the fix together.
Root cause
The Go runtime randomizes map iteration start and sequence to prevent programs from depending on order. Any output whose sequence derives from a map range is therefore nondeterministic, and assertions against fixed expectations fail at a rate proportional to the number of orderings.
Fix
Sort keys explicitly before assembling ordered output (slices.Sorted(maps.Keys(m))), design serializers to emit stable field order, and make tests compare order-insensitively when order is irrelevant. Run -count in CI for flake detection.
Risk note
Comparing sorted-by-accident output today encodes an undocumented dependency. Test the contract you actually intend, sorted or set-based.
Verify the fix
Run the suite with -count=100 and assert zero failures; add a CI job that periodically runs the flake-prone packages with elevated counts.
Symptom walkthrough
The flake has a rate, not a cause you can see in one failing log:
$ go test -count=50 -run TestBuildOrder ./gen/
--- FAIL: TestBuildOrder (0.00s)
gen_test.go:28: got "a,b,c" want "c,a,b"
FAIL
# 47 passed, 3 failed
Three failures in fifty with the same code and input is the randomized-order fingerprint — timing races trend differently, growing under load. The mismatch itself shows two orderings of the same elements. Trace the construction of the got string and you will find a for k := range m feeding a strings.Join or a Marshal call.
Fix in practice
Sort at the assembly point:
import (
"maps"
"slices"
)
keys := slices.Sorted(maps.Keys(m)) // deterministic order
out := strings.Join(keys, ",")
When order is genuinely irrelevant, compare as sets so the test encodes the real contract:
diff := cmp.Diff(
slices.Sorted(maps.Keys(want)),
slices.Sorted(maps.Keys(got)),
)
And in JSON output paths, prefer marshalling structs with fixed field order over maps with random key order — the stability then belongs to the serializer, not to each caller remembering to sort.