What is escape analysis, and how does it decide stack vs. heap allocation?
Quick Answer
Escape analysis is a compile-time analysis the Go compiler performs to decide whether a variable can be safely allocated on the stack (fast, automatically freed when the function returns) or must "escape" to the heap (slower, managed by the garbage collector) because it's still referenced after the function returns. A variable escapes if its address is returned, stored somewhere that outlives the function (like a global or a struct field on a heap-allocated object), captured by a closure that outlives the function, or passed to something the compiler can't fully analyze (like an interface{} parameter or a function called through a pointer). You can see the compiler's actual decisions with go build -gcflags="-m", which prints escapes to heap for each variable that couldn't stay on the stack.
Detailed Answer
Escape analysis is why Go can often skip heap allocation, and GC involvement, entirely for many local variables.
A variable that stays on the stack
func sum(a, b int) int {
total := a + b // never referenced outside sum — stays on the stack
return total
}
total never outlives sum, so the compiler allocates it on the stack, which is essentially free to allocate and free (just moving a stack pointer), with zero GC involvement.
A variable that escapes to the heap
func newCounter() *int {
count := 0
return &count // count's address escapes — must live on the heap
}
Since newCounter returns a pointer to count, and the caller can use that pointer long after newCounter returns, count can't safely live on a stack frame that's about to be destroyed. The compiler allocates it on the heap instead.
Checking the compiler's actual decisions
go build -gcflags="-m" ./...
./main.go:5:2: total escapes to heap
./main.go:10:9: &count escapes to heap
This flag prints exactly which variables escaped and (often) why, which is the standard way to investigate unexpected allocations in a performance-sensitive hot path.
Common causes of unwanted escapes
func process(v interface{}) { ... } // passing a concrete value as `any` often forces it to the heap
process(myStruct) // myStruct may escape here
func makeClosure() func() int {
x := 0
return func() int { x++; return x } // x escapes: the closure captures and outlives the function
}
Why this matters for performance
Heap allocations cost more than stack allocations, and every heap allocation adds work for the garbage collector later. In a hot loop, an unexpected escape can be a real, measurable performance cost. Passing a value as interface{}, or returning a pointer to a local, are common causes. That's exactly why -gcflags="-m" and profiling are standard tools for optimizing allocation-heavy Go code.