Go Escape Analysis — Cheatsheet
Quick reference · pairs with Lesson 6
The rule
A value lives on the STACK if the compiler can prove it does
not outlive its function. Otherwise it escapes to the heap.
Decided at compile time, per value. Not about new, not about pointer-vs-value —
only about proven lifetime.
Inspect it
go build -gcflags=-m ./... # escape + inline decisions
go build -gcflags='-m -m' ./pkg # verbose reasoning
go test -gcflags=-m -run=x ./pkg # same, on test builds
go test -bench=. -benchmem # allocs/op & B/op — the payoff metric
What the output means
-m line | Meaning |
moved to heap: x | x escaped — heap allocated |
&x escapes to heap | the address of x leaks past the frame |
x escapes to heap | x boxed into interface / leaked |
x does not escape | stays on the stack — free |
inlining call to f | f's body pasted in; can change escape results |
Escapes to heap
Forces the heap
- Return a pointer to a local
- Store a pointer in a heap object / global / slice / map / channel
- Pass into
interface{} (e.g. fmt.Println, JSON, reflection)
- Captured by an escaping closure or
goroutine
- Runtime-unknown size (
make(T, n), growing append)
- Value too big for the stack frame
Stays on the stack
- Local value returned by copy
new(T) / &x whose pointer never leaves the frame
- Address passed to a function that provably doesn't retain it (often inlined)
- Fixed-size, frame-local scratch buffers
Cost model
| | Stack | Heap |
| Allocate | bump pointer — ~free | allocator + GC bookkeeping |
| Free | frame pop | GC cycle (later) |
In allocs/op | no | yes |
| OOM risk | bounded by frame | grows with live set × request rate |
Mission connection (OOM)
A whole-file
[]byte read is runtime-sized
and returned →
heap,
sized to the file. Multiply by concurrent requests → OOM. Streaming (
io.Pipe,
io.Copy) keeps only a small fixed buffer live per request. See
Lesson 3.