Escape Analysis: Pointer Escapes, Interface Wrapping, Stack Growth
- Last updated
- Prerequisites:
- Go pointers and stack/heap basics
- Understanding of compiler optimizations
- go
- escape-analysis
- stack
- heap
- allocation
- compiler
Read at your depth
The practical view
Escape analysis is a compile-time optimization that decides whether a value can live on the goroutine's stack instead of the heap. If a value doesn't escape the function (no pointer returned, not stored in a global, not passed to an interface that leaks it), the compiler allocates it on the stack — zero GC pressure and fast allocation. If it escapes, it's heap-allocated. You can see the decision with go build -gcflags='-m' (allocs: x escapes to heap). This is why small local values are cheap; interfaces and pointers to returned values often trigger escapes.
The same idea in other frameworks
N/A — Go compiler concern.N/A — Go concern.N/A — Go concern.N/A — Go concern.Legacy vs modern
Value that escapes (via interface) vs value that stays on the stack
Passing a value to an interface forces a heap allocation; keeping it concrete lets escape analysis stack-allocate it.
var v any = 42 // ❌ OLD: interface conversion — escapes to heapfunc sum(n int) int { // ✅ NEW: local ints stay on the stack
var total int
for i := 0; i < n; i++ { total += i }
return total
}Interview gotchas
Context
This tests the escape analysis triggers — interviewers want the concrete conditions (returned pointers, interfaces, globals, closures) and the tooling to observe them.
The mechanical answer
A value escapes when the compiler cannot prove it stays within the current stack frame. Common triggers: returning a pointer to it (or a struct containing one), storing it in a global, storing it in a heap-allocated structure (map/slice that outlives the call), passing it to a function that retains the reference (e.g. storing the argument in a field or a closure), and interface conversion — an interface value carries a pointer to the underlying data, so boxing a value into an interface (including any, fmt.Println, etc.) makes it escape. Closures escape when the closure itself escapes. To find escapes, use go build -gcflags='-m' (and -m -m for more detail) and look for 'escapes to heap'. The fix is usually to avoid interfaces/pointers in hot paths or restructure so the value stays local.
Trap
The naive answer is 'pointers always escape'. A pointer used only within the function (or within inlined boundaries) can stay on the stack; returning it forces escape. Another trap: assuming small values in interfaces are cheap — any interface boxing of a value escapes and allocates. Candidates who enumerate the triggers and the -m tooling show real understanding.
Context
This probes the stack model — interviewers want the contiguous growable stack and why stack allocation is cheaper than heap allocation + GC for short-lived values.
The mechanical answer
Stack allocation is nearly free: the compiler assigns a stack slot, and the value dies when the frame pops — no allocation call, no GC involvement, and excellent cache locality (stack frames are contiguous). Heap allocation requires a call into the allocator, participates in GC scanning, and adds pressure. To allow large frames, Go uses contiguous growable stacks: a goroutine starts with a small stack (2KB), and when it runs out, the runtime copies the entire stack to a larger contiguous region and updates pointers (via stack copying, which requires all pointers to be located — the compiler emits stack maps). This is why deep recursion is safe in Go (unlike fixed-size stacks). The escape analysis decides which values use the stack vs heap, and the stack growth mechanism makes 'just stack it' safe even for large frames.
Trap
The naive answer is 'stack allocation is only for tiny values'. Go's growable stack means size isn't the deciding factor — escape is. Another trap: assuming heap allocation is always bad — long-lived values (a server's shared state) SHOULD be on the heap; escape analysis optimizes the short-lived case. Candidates who describe stack copying and the escape decision show real Go runtime understanding.