Lesson 22 · Testing, tooling & performance
Benchmarks & pprof
Measure, don't guess — the tooling for making Go fast.
Your win: write and run a benchmark, read ns/op /
allocs/op, and know how to pull a CPU/memory profile with pprof
— the "how would you find a bottleneck?" answer.
Benchmarks are built in too
A benchmark is func BenchmarkXxx(b *testing.B) in a _test.go
file. In Go 1.24+ (our 1.25) the robust form is for b.Loop(), which times
only the loop and blocks misleading compiler optimisations:1
func BenchmarkRender(b *testing.B) {
in := setup() // runs once, not timed
for b.Loop() { // modern form (Go 1.24+)
Render(in)
}
}
// older form: for i := 0; i < b.N; i++ { ... }
$ go test -bench=Render -benchmem
BenchmarkRender-8 1.2M 980 ns/op 256 B/op 4 allocs/op
ns/op — time per operation (speed). B/op — bytes allocated
per op. allocs/op — heap allocations per op. Allocations are often the
real cost: fewer allocs usually means faster and less GC pressure (Lesson 23).
-benchmem turns the last two on.
Profiling with pprof
Benchmarks tell you how slow; a profile tells you where. Capture one, then explore it:2
$ go test -bench=Render -cpuprofile cpu.out -memprofile mem.out
$ go tool pprof cpu.out
(pprof) top # hottest functions
(pprof) list Render # line-by-line time inside a function
(pprof) web # visual call graph
For a live server, importing net/http/pprof exposes
/debug/pprof/ so you can profile production traffic:
go tool pprof http://host/debug/pprof/profile.
Escape analysis — where allocations come from
Go decides whether a value lives on the stack (cheap, auto-freed) or escapes to the heap (costs an allocation + GC work). See its decisions with:
$ go build -gcflags=-m ./...
./x.go:12:9: &buf escapes to heap
Reducing heap escapes is the most common Go optimisation — and the reason
allocs/op matters.3
EmailMetrics) and tracing spans (interceptors.StartSpan,
seen in every repo method). Different tools, same goal: know where time goes before
you change anything.
The Go Blog — "Profiling Go Programs" + testing.B.Loop
A real worked profiling session (10× speedup), plus the modern benchmark form. 100 Go Mistakes has an excellent benchmarking-pitfalls chapter.
Check yourself (from memory)
Q1. A Go benchmark function has the signature…
BenchmarkXxx(b *testing.B), looping with
b.Loop() (or the older b.N).
Q2. go test -bench=. -benchmem additionally reports…
B/op and allocs/op —
allocations, which often dominate cost and GC pressure.
Q3. You inspect a captured CPU profile with…
go tool pprof — then top,
list, web to find the hot spot.
func BenchmarkFn(b *testing.B){ for
b.Loop(){ Fn(in) } }. (2) Run go test -bench=BenchmarkFn -benchmem
→ read ns/op, B/op, allocs/op. (3) Profile:
-cpuprofile cpu.out, then go tool pprof cpu.out
(top/list/web) to find the hot spot. (4) For
allocs, -memprofile + go build -gcflags=-m for escapes.
Optimise the proven hot spot, then re-benchmark.ns/op drop? Ask me — hands-on practice.
1. The Go Blog — More predictable benchmarking with testing.B.Loop.
2. The Go Blog — Profiling Go Programs.
3. 100 Go Mistakes — allocations & benchmarking.