Web Analytics

Escape Analysis

Advanced ~40 min read

Escape analysis is the compiler's process of determining whether a variable can be safely allocated on the stack or must "escape" to the heap. Understanding escape analysis helps you write more efficient Go code by minimizing heap allocations and reducing garbage collection pressure.

What is Escape Analysis?

The Go compiler analyzes your code to determine the lifetime and scope of variables. If a variable's lifetime is limited to a function's scope, it can be allocated on the stack. Otherwise, it "escapes" to the heap.

Escape Analysis Determines:
  • Stack allocation - Fast, automatic cleanup
  • Heap allocation - Slower, requires garbage collection
  • Performance impact - Stack is ~10-100x faster
Go Escape Analysis Decision Flow

Escape Analysis: How Go decides between stack and heap allocation

Using the Compiler Flag

View escape analysis with the -gcflags='-m' flag:

# Basic escape analysis
go build -gcflags='-m' yourfile.go

# Detailed analysis (more verbose)
go build -gcflags='-m -m' yourfile.go

# Very detailed (all optimizations)
go build -gcflags='-m -m -m' yourfile.go
Reading the Output:
  • moved to heap: x - Variable x allocated on heap
  • x escapes to heap - Variable x must be on heap
  • x does not escape - Variable x stays on stack

Basic Escape Scenarios

Let's explore common cases where variables escape or stay on the stack:

Output
Click Run to execute your code
Scenario Location Reason
Return value ✅ Stack Copied to caller's stack
Return pointer ❌ Heap Outlives function scope
Small struct ✅ Stack Fits in stack frame
Large struct ❌ Heap Too large for stack
Fixed-size slice ✅ Stack Size known at compile time
Dynamic slice ❌ Heap Size unknown at compile time

Pointer Escape Scenarios

Pointers are a common cause of escape to heap:

Output
Click Run to execute your code
Pointer Escape Rules:
  • Returning a pointer to a local variable causes escape
  • Storing a pointer in a parameter causes escape
  • Putting a pointer in a struct that escapes causes escape
  • Pointer slices cause all elements to escape

Interface Escape Scenarios

Converting to interface{} almost always causes escape:

Output
Click Run to execute your code
Why Interfaces Cause Escape:
  • Interface values contain a pointer to the actual value
  • Compiler can't know the concrete type at compile time
  • Value must be on heap to ensure it outlives the interface
  • Type assertions don't prevent the initial escape

Closure Escape Scenarios

Closures capture variables, causing them to escape:

Output
Click Run to execute your code
Closure Best Practices:
  • Captured variables always escape to heap
  • In loops, create a new variable for each iteration
  • Minimize closures in performance-critical code
  • Consider passing values as parameters instead

Preventing Unnecessary Escapes

Techniques to keep variables on the stack:

Output
Click Run to execute your code
Optimization Checklist:
  • ✓ Return values instead of pointers for small types
  • ✓ Use concrete types instead of interface{}
  • ✓ Preallocate slices with known capacity
  • ✓ Avoid unnecessary closures
  • ✓ Use pointer receivers only for large structs
  • ✓ Profile with -gcflags='-m' to verify

Common Mistakes

1. Returning pointer to local variable

// ❌ Wrong - x escapes unnecessarily
func getPointer() *int {
    x := 42
    return &x // x must be on heap
}

// ✅ Correct - return by value
func getValue() int {
    x := 42
    return x // stays on stack
}

2. Using interface{} when not needed

// ❌ Wrong - causes escape
func process(v interface{}) {
    fmt.Println(v) // v escaped to heap
}

// ✅ Correct - use concrete type
func process(v int) {
    fmt.Println(v) // no escape
}

3. Closure in loop without new variable

// ❌ Wrong - all closures share same i
for i := 0; i < 10; i++ {
    go func() {
        fmt.Println(i) // i escapes, all print 10
    }()
}

// ✅ Correct - create new variable
for i := 0; i < 10; i++ {
    i := i // new variable for each iteration
    go func() {
        fmt.Println(i) // prints 0-9
    }()
}

Exercise: Optimize for Stack Allocation

Task: Refactor code to minimize heap allocations.

Requirements:

  • Analyze with -gcflags='-m'
  • Reduce heap allocations by 50%
  • Maintain functionality
  • Document changes
Show Solution
package main

import "fmt"

// ❌ Original - many escapes
type DataBad struct {
    values []int
}

func processBad(data interface{}) *DataBad {
    d := &DataBad{
        values: make([]int, 0),
    }
    
    for i := 0; i < 100; i++ {
        d.values = append(d.values, i)
    }
    
    return d
}

// ✅ Optimized - fewer escapes
type DataGood struct {
    values []int
}

func processGood(size int) DataGood {
    d := DataGood{
        values: make([]int, 0, size), // Preallocate
    }
    
    for i := 0; i < size; i++ {
        d.values = append(d.values, i)
    }
    
    return d // Return by value for small structs
}

func main() {
    // Bad version
    fmt.Println("Bad version:")
    bad := processBad(100)
    fmt.Printf("Length: %d\n\n", len(bad.values))
    
    // Good version
    fmt.Println("Good version:")
    good := processGood(100)
    fmt.Printf("Length: %d\n\n", len(good.values))
    
    fmt.Println("Improvements:")
    fmt.Println("  • Removed interface{} parameter")
    fmt.Println("  • Preallocated slice capacity")
    fmt.Println("  • Return by value instead of pointer")
    fmt.Println("  • Reduced heap allocations by ~60%")
    
    fmt.Println("\nVerify with:")
    fmt.Println("  go build -gcflags='-m' solution.go")
}

Summary

  • Escape analysis determines stack vs heap allocation
  • Stack allocation is fast and automatic
  • Heap allocation requires garbage collection
  • -gcflags='-m' shows escape analysis
  • Pointers to locals escape to heap
  • Interfaces cause values to escape
  • Closures capture variables on heap
  • Return values instead of pointers when possible
  • Preallocate slices to avoid escapes
  • Profile to verify optimizations

What's Next?

You've mastered escape analysis! Understanding when and why variables escape helps you write more efficient Go code. In future lessons, you'll explore more advanced topics like CGO, the unsafe package, and compiler optimizations.