Go cookbook · Intermediate
Benchmark code with testing.B.Loop
You want to know whether an optimisation helps and how many allocations it makes.
Approach
Write benchmarks with for b.Loop() (Go 1.24+), which handles timing and keeps the compiler from optimising
the work away. Report allocations, compare variants with sub-benchmarks, and use benchstat for statistics.
Solution
package text
import (
"strings"
"testing"
)
func JoinPlus(parts []string) string {
s := ""
for _, p := range parts {
s += p + ","
}
return s
}
func JoinBuilder(parts []string) string {
var b strings.Builder
for _, p := range parts {
b.WriteString(p)
b.WriteByte(',')
}
return b.String()
}
func BenchmarkJoin(b *testing.B) {
parts := strings.Fields(strings.Repeat("tea pot mug kettle ", 50))
b.Run("plus", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
JoinPlus(parts)
}
})
b.Run("builder", func(b *testing.B) {
b.ReportAllocs()
for b.Loop() {
JoinBuilder(parts)
}
})
}
go test -bench=Join -benchmem -count=10 ./text > new.txt
go run golang.org/x/perf/cmd/benchstat@latest old.txt new.txt
Pitfalls
- Using
b.Nloops with setup inside the loop; withb.Loop()setup before the loop isn't timed. - Drawing conclusions from one run on a noisy laptop; use
-countand benchstat. - Benchmarking unrealistic inputs.
Variations
- Profile with
-cpuprofile cpu.outandgo tool pprof cpu.out. - Use profile-guided optimisation (
default.pgo) with production profiles.
Further reading: testing.B.Loop
Reference: https://pkg.go.dev/testing#B.Loop
More Go recipes
- Build a small, secure container image for a Go service
- Decode and validate JSON request bodies safely
- Embed static files and templates in the binary
- Load and validate configuration from environment variables
- Log structured JSON with slog and redact secrets
- Process jobs concurrently with a bounded worker pool
- Profile a running service with pprof
- Rate limit requests per client