How do you write a test that fails if a hot path starts allocating? Explain testing.AllocsPerRun and its caveats.
Question 403HardGo 1.22 to 1.25
Benchmarks show allocs/op, but nobody reads benchmark output on every PR. For code that must stay allocation-free (encoders, loggers, request routing), lock it in with a regular unit test using testing.AllocsPerRun(runs, f). It sets GOMAXPROCS(1), calls f once to warm up, then runs it runs times and returns the average number of heap allocations per call.
func appendRecord(dst []byte, id int, status string) []byte {
dst = append(dst, "id="...)
dst = strconv.AppendInt(dst, int64(id), 10)
dst = append(dst, " status="...)
return append(dst, status...)
}
func TestAppendRecordNoAlloc(t *testing.T) {
if testing.CoverMode() != "" {
t.Skip("coverage instrumentation can change allocation behavior")
}
buf := make([]byte, 0, 64)
allocs := testing.AllocsPerRun(1000, func() {
buf = appendRecord(buf[:0], 42, "ok")
})
if allocs != 0 {
t.Fatalf("appendRecord allocs/op = %v, want 0", allocs)
}
}
Caveats:
- The result is an integer average (total mallocs / runs, integer division), so an allocation that happens once every few calls (e.g. amortized buffer growth) reports 0. Use enough runs and a buffer that is already large enough.
- Don't use it in a test that calls
t.Parallel(): it changesGOMAXPROCSand counts every allocation in the process, so concurrent tests pollute the count. - Instrumentation changes results: the race detector and coverage can add or move allocations. The standard library skips these tests under
-race(via its internalrace.Enabled); in your own code, use a build-tagged constant ortesting.CoverMode(). - When it fails, find the cause with
go build -gcflags=-m("escapes to heap") or a-memprofilefrom the equivalent benchmark.
Interviewer is looking for: turning a performance property into a regression test instead of hoping someone notices a benchmark change.
More on Performance, Profiling & Testing
- Q401What are Example functions, and how are they verified?
- Q402Walk through how you would investigate a service whose p99 latency doubled after a deploy.
- Q404How do you benchmark concurrent code with b.RunParallel, and why does sync.RWMutex often fail to scale for read-heavy workloads?
- Q405Go 1.24 replaced the built-in map implementation with Swiss tables. What changed, and what does it mean for performance-sensitive code?
- Q406Why do pointers make the GC more expensive, and how can data layout, unique.Handle, and weak pointers reduce memory and GC cost?