What is escape analysis, and how does it decide stack vs. heap allocation?

6 minadvancedgoescape-analysismemoryperformance

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.