Problem Statement
Your Go microservice is generating excessive garbage, causing GC pressure. Using escape analysis, determine which allocations are escaping to the heap and optimize them to stay on the stack.
What is Escape Analysis?
The Go compiler analyzes whether a variable's lifetime extends beyond its function. If it does, the variable "escapes" to the heap. Stack allocation is faster and doesn't require GC.
Viewing Escape Analysis
go build -gcflags="-m" ./...
# More verbose:
go build -gcflags="-m -m" ./...
# Output examples:
# ./main.go:10:2: moved to heap: x
# ./main.go:15:9: &User{} escapes to heapCommon Escape Scenarios
package main
// 1. Returning pointer to local variable (ESCAPES)
func createUser() *User {
u := User{Name: "Alice"} // Escapes to heap
return &u
}
// 2. Storing pointer in long-lived structure (ESCAPES)
var globalUsers []*User
func addUser(name string) {
u := &User{Name: name} // Escapes
globalUsers = append(globalUsers, u)
}
// 3. Passing to interface{} (ESCAPES - boxing)
func logValue(v interface{}) {
fmt.Println(v)
}
func example() {
x := 42
logValue(x) // x escapes (boxed into interface)
}
// 4. Closure capturing by reference (ESCAPES)
func makeCounter() func() int {
count := 0 // Escapes
return func() int {
count++
return count
}
}
// 5. Large stack allocation (ESCAPES)
func bigArray() {
arr := [1000000]int{} // Too large for stack, escapes
_ = arr
}Optimizations to Avoid Escape
// ❌ Escapes: pointer return
func newBuffer() *bytes.Buffer {
var buf bytes.Buffer
return &buf
}
// ✅ Stack: value return (caller allocates)
func newBuffer() bytes.Buffer {
var buf bytes.Buffer
return buf
}
// ❌ Escapes: interface parameter
func process(r io.Reader) { ... }
// ✅ Stack: concrete type when possible
func processFile(r *os.File) { ... }
// ❌ Escapes: slice grows unpredictably
func collect() []int {
var result []int
for i := 0; i < 100; i++ {
result = append(result, i)
}
return result
}
// ✅ Pre-allocated: known size, less pressure
func collect() []int {
result := make([]int, 0, 100)
for i := 0; i < 100; i++ {
result = append(result, i)
}
return result
}Benchmark Stack vs Heap
type Data struct {
values [100]int
}
// Stack allocation
func stackAlloc() Data {
return Data{}
}
// Heap allocation
func heapAlloc() *Data {
return &Data{}
}
// Benchmark results:
// BenchmarkStackAlloc 500000000 3.2 ns/op 0 B/op 0 allocs/op
// BenchmarkHeapAlloc 20000000 85.0 ns/op 896 B/op 1 allocs/opFollow-up Questions
- Why does passing a value to
interface{}cause escape? - How large can a stack allocation be before it escapes?
- What is the "tiny allocator" optimization?