← Debugging Guides

阅读中文版本

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.

beginnerdebugginggomapstesting

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 -3

Interpretation

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/ | head

Interpretation

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.