Skip to content

Profiling & Performance

Go ships production-grade profiling tools in the standard library — there’s rarely a need to reach for third-party APM just to answer “where is the time going?”

Before optimizing anything, measure it with go test -bench:

func BenchmarkParseJSON(b *testing.B) {
data := []byte(`{"name":"Ada","age":30}`)
for i := 0; i < b.N; i++ {
var u User
json.Unmarshal(data, &u)
}
}
Terminal window
go test -bench=. -benchmem

-benchmem adds allocations-per-operation to the output — often the more actionable number, since allocation pressure drives GC overhead.

Add pprof to any long-running server with a one-line import:

import _ "net/http/pprof"
// somewhere in main:
go http.ListenAndServe("localhost:6060", nil)

Then, while the program is under load:

Terminal window
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof http://localhost:6060/debug/pprof/heap

Inside the interactive pprof shell, top shows the hottest functions and web renders an SVG call graph (needs Graphviz installed).

For a specific function rather than a whole running server:

Terminal window
go test -bench=BenchmarkParseJSON -cpuprofile=cpu.out -memprofile=mem.out
go tool pprof cpu.out

Data races don’t always crash — they can silently corrupt data. Catch them at test time:

Terminal window
go test -race ./...
go run -race main.go

The race detector instruments every memory access and slows execution significantly, so it’s normally used in CI and local testing, not production.

  1. Reduce allocations — reuse buffers with sync.Pool, pre-size slices/maps with make(..., 0, n).
  2. Avoid unnecessary interface boxing in hot loops — concrete types avoid an allocation + indirection.
  3. Batch I/O — one query for 100 rows beats 100 queries.
  4. Only then consider more goroutines — concurrency helps with I/O-bound waits, not CPU-bound work beyond your core count.