Reading — step 1 of 7
Learn
Go's GC is fast, but unnecessary heap allocations still hurt — they pressure the GC, blow caches, and run slower than stack memory. The compiler decides whether each value lives on the stack or the heap; understanding escape analysis lets you write code that stays on the stack.
Stack vs heap in Go
- Stack allocations are nearly free — bump a pointer, zero on return
- Heap allocations cost: GC tracking, atomic alloc paths, cache misses, eventual GC sweep
- The compiler chooses for you, based on whether a value's pointer escapes the function
Seeing escape decisions
go build -gcflags="-m -l" ./...
Produces output like:
./main.go:8:9: &User{...} escapes to heap
./main.go:14:13: leaking param: x
./main.go:18:6: moved to heap: result
-m shows the compiler's escape and inlining decisions. -l disables inlining (clearer output for diagnosis).
What causes escape
A value escapes if its pointer leaves the function. Common patterns:
Common allocation sources to watch
- interface{} parameters and returns — the value is boxed (heap)
- fmt.Sprintf for hot paths — allocates a new string each call
- append with insufficient capacity — every grow re-allocates the backing array
- Maps with growing keys — internal restructuring allocates
- Defer with closures — each defer frame is an allocation in older Go versions (mostly fixed in 1.14+)
- Strings + concatenation — each
+produces a new string
Pre-allocate slices
Goes from O(log n) reallocations to zero. Big win for hot paths.
strings.Builder for concatenation
For long strings, strings.Builder is dramatically faster.
sync.Pool for high-frequency reuse
Recycles buffers between requests. Standard pattern in HTTP middleware, JSON encoders.
Profiling
go test -cpuprofile=cpu.prof -memprofile=mem.prof -bench=.
Then go tool pprof cpu.prof opens an interactive profiler. (pprof) top shows the hottest functions; (pprof) list FuncName shows source-annotated time. Production servers expose /debug/pprof/ via net/http/pprof.
When to optimize
Go's GC is fast. The vast majority of code doesn't need allocation analysis. Reach for it when:
- Profiling has identified a real hot path
- The code runs at >1k QPS
- Latency p99 matters (GC pauses)
Premature optimization in Go is the same as in any language — measure first, optimize second.
Common mistakes
- Optimizing without profiling — the bottleneck is rarely where you think.
- Over-using sync.Pool — adds complexity. Worth it only when allocation is genuinely the bottleneck.
- Caring about escape for cold paths — only matters in hot loops; cold code doesn't move the needle.
- Trusting microbenchmarks too much — Go's compiler can optimize away unused results. Use the result with
b.Logor assign to a global.
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…