Escape Analysis
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.
- Stack allocation - Fast, automatic cleanup
- Heap allocation - Slower, requires garbage collection
- Performance impact - Stack is ~10-100x faster
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
moved to heap: x- Variable x allocated on heapx escapes to heap- Variable x must be on heapx does not escape- Variable x stays on stack
Basic Escape Scenarios
Let's explore common cases where variables escape or stay on the stack:
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:
Click Run to execute your code
- 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:
Click Run to execute your code
- 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:
Click Run to execute your code
- 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:
Click Run to execute your code
- ✓ 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.
Enjoying these tutorials?