Performance in Go: Measure, Profile, Then Optimize
Go is fast enough for most services without heroic tuning.
When latency or cost matter, the language culture is explicit: prove the bottleneck with data, fix the proven hot path, and keep code readable everywhere else.
Performance Basics collects runnable measurement snippets; sibling articles cover pprof, tracing, escape analysis, allocation patterns, PGO, and GC tuning.
Summary
- Performance work in Go follows a measurement loop - benchmark or load test, profile under realistic traffic, change one variable, re-measure, and document the delta.
- Insight: Micro-optimizations without profiles waste review time and often regress clarity. The stdlib and runtime already expose the tools you need without third-party profilers.
- Key Concepts: benchmarks (
testing.B), pprof (CPU/heap/mutex/block), execution tracer (go tool trace), escape analysis, allocations, GOGC/GOMEMLIMIT, PGO. - When to Use: Tail latency SLO misses, high cloud bills from CPU saturation, GC pause spikes, or a PR that claims speedups without evidence.
- Limitations/Trade-offs: Profiles sample; short benchmarks lie; GC tuning is a last resort;
unsafeand pools add maintenance cost that must beat measured gains. - Related Topics: testing/benchmarking culture, net/http client pooling, concurrency patterns, observability metrics, and Performance Rules: When to Optimize.
Foundations
Go programs spend time in three broad buckets: your code, the runtime (scheduler, GC, reflection), and the OS (syscalls, network, disk).
Optimization starts by identifying which bucket dominates for your workload.
Benchmarks (go test -bench) answer micro-questions: is implementation A faster than B on a fixed input size?
They run in a controlled process with warm CPU caches and are ideal for serialization, parsing, and tight loops.
Profiles (runtime/pprof, net/http/pprof) answer macro-questions: which functions consume CPU or heap under production-like concurrency?
CPU profiles are statistical samples of the call stack.
Heap profiles show where allocations originate and how much live memory each call site retains.
Traces (runtime/trace) capture a timeline: goroutine scheduling, GC STW events, syscall blocking, and network waits.
Use traces when pprof shows low CPU but latency is still high.
The compiler inlines small functions, eliminates bounds checks when it can prove safety, and applies escape analysis to decide stack vs heap allocation.
You influence it with idiomatic code more often than with //go:noinline directives.
The garbage collector is generational and concurrent.
Allocation rate drives GC CPU and pause behavior.
Reducing pointers and reusing buffers often beats turning GOGC knobs.
Mechanics & Interactions
A typical investigation loop looks like this:
SLO alert or bench regression
|
v
Reproduce with realistic load / payload size
|
+-----+-----+
| |
v v
go test HTTP/gRPC
-bench load + pprof
| |
+-----+-----+
v
Identify hot func or alloc site
|
v
One change + recorded before/after
|
v
Ship if SLO/clarity trade-off is documented
| Tool | Question it answers | Typical signal |
|---|---|---|
go test -bench | Is this function faster? | ns/op, allocs/op |
| CPU pprof | Where is CPU time? | Flat % in one func |
| Heap pprof | Who allocates? | alloc_space, inuse_space |
go tool trace | Why goroutines wait? | Long syscall or GC bars |
go build -gcflags=-m | Does value escape? | moved to heap lines |
PGO (default.pgo) | Steady-state hot paths? | Compiler inlining hints |
Benchmarks and profiles complement each other.
A benchmark might show 30% faster JSON encoding, while a heap profile reveals the win came from fewer allocations that also reduced GC pause.
Always pair timing with -benchmem when allocations are plausible.
For services, expose net/http/pprof on an admin port or pull profiles with go tool pprof http://host:6060/debug/pprof/profile.
Collect profiles during load, not on an idle process, or the sample will be empty noise.
Advanced Considerations & Applications
Latency-sensitive services (payments, auth, realtime APIs) care about tail percentiles, not average CPU.
Traces expose queueing behind mutexes, channel backpressure, and STW GC segments that averages hide.
Fix contention and allocation first; only then experiment with GOGC or GOMEMLIMIT.
Throughput workers (ETL, batch indexers) often bottleneck on IO.
CPU profiles may show syscall or compress dominance.
Parallelism, buffer sizing, and fewer copies beat shaving nanoseconds in hash functions.
Profile-Guided Optimization (PGO) feeds production CPU profiles into the compiler via default.pgo.
It helps stable binaries where hot paths do not change every commit.
Regenerate the profile on release branches, not from a one-off dev laptop trace.
Escape analysis explains why a small struct still allocates: returning a pointer to a local, storing into an interface{}, or capturing a variable in a closure that outlives the stack frame.
Sometimes the fix is returning by value; sometimes it is accepting one allocation as cheaper than fighting the compiler.
When to stop: SLOs are green, pprof flat graphs are spread across many small functions, and further changes harm readability.
Record that decision in the PR or runbook so the next engineer does not re-open settled tuning.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
| Benchmarks | Fast, CI-friendly | Synthetic input risk | Libraries, encoders |
| pprof | Ground truth under load | Needs representative traffic | Services |
| Trace | Scheduler/GC visibility | Larger artifacts | Tail latency mysteries |
| PGO | Compiler sees prod paths | Profile drift | Long-lived binaries |
| GC knobs | Quick experiments | Masking alloc bugs | Last resort after alloc fixes |
Common Misconceptions
go test -benchalone proves production performance - Bench environment differs from networked, contended, and cached production paths. Use it to compare implementations, then validate with load tests.- Low CPU means no performance problem - Goroutines blocked on IO, locks, or GC still miss latency SLOs. Reach for
go tool tracewhen CPU pprof is flat. - Preallocate everything - Unused capacity wastes memory and can increase GC work. Preallocate when size is known or measured.
- pprof is only for experts -
go tool pprof -topand flame graphs in the web UI are enough for most first passes. - Optimize before readability - Go reviewers default to clear code. Keep evidence when you bypass that norm.
FAQs
Where do I start when latency regresses?
Reproduce with production-sized payloads and concurrency.
Capture a CPU profile and a heap profile under that load, then read the top few functions before editing code.
Are benchmarks enough for HTTP services?
Benchmarks help handler helpers and serializers.
End-to-end latency needs load tests plus pprof or tracing on the running server.
What is the difference between CPU and heap profiles?
CPU profiles show where time is spent executing code.
Heap profiles show allocation sites and retained memory, which often drive GC cost.
When should I use the execution tracer?
Use it when CPU usage looks healthy but requests still queue or stall.
Traces reveal scheduling, syscall, and GC timing on a timeline.
Does Go 1.26 change performance guidance?
Green Tea GC is the default collector in recent releases, but the measure-profile-optimize loop is unchanged.
Re-benchmark after toolchain upgrades because compiler and runtime behavior evolve.
Is escape analysis only for advanced users?
No.
Run go build -gcflags=-m on a package when a benchmark shows unexpected allocations.
The compiler tells you which values escape to the heap.
When is PGO worth the workflow cost?
When you ship long-lived binaries with stable hot paths and want the compiler to inline and layout code using production profiles.
Skip PGO for fast-moving code paths or CLI tools with diverse inputs.
Should I tune GOGC before reducing allocations?
Usually no.
Lower allocation pressure fixes root cause; GOGC experiments are for latency after alloc work is exhausted.
Can I profile in CI?
Yes for benchmarks and regression guards.
Full pprof under load belongs in staging or controlled production windows with auth on debug endpoints.
How does this section relate to go-rules performance rules?
Performance Rules: When to Optimize is the policy cheatsheet.
This section is the hands-on tooling guide for executing that policy.
Related
- Performance Basics - runnable measurement starting points
- CPU & Heap Profiling with pprof - profiles under load
- trace & Execution Tracer - scheduler and GC timelines
- Benchmarks with testing.B & B.Loop - micro-benchmark depth
- Performance Rules: When to Optimize - when to stop tuning
- Performance Best Practices - team optimization checklist
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).