Memory Management and Escape Analysis
Go’s memory management is largely automatic, but understanding escape analysis helps you write more efficient code. Escape analysis determines whether variables are allocated on the stack or heap.
Stack vs Heap
Stack Allocation
Stack allocation is fast and automatically freed when the function returns.
package main
import (
"fmt"
)
func stackAllocation() {
// Allocated on stack - fast, automatic cleanup
x := 42
y := "hello"
z := []int{1, 2, 3}
fmt.Println(x, y, z)
// All automatically freed when function returns
}
func main() {
stackAllocation()
}
Heap Allocation
Heap allocation is slower but persists beyond function scope.
package main
import (
"fmt"
)
func heapAllocation() *int {
// Allocated on heap - slower, requires GC
x := 42
return &x // Escapes to heap
}
func main() {
ptr := heapAllocation()
fmt.Println(*ptr)
}
Understanding Escape Analysis
Escape analysis determines if a variable “escapes” its function scope.
Good: No Escape
package main
import (
"fmt"
)
func noEscape() int {
// x stays on stack
x := 42
y := 10
return x + y
}
func main() {
result := noEscape()
fmt.Println(result)
}
Bad: Unnecessary Escape
// โ AVOID: Returning pointer when not needed
package main
import (
"fmt"
)
func unnecessaryEscape() *int {
// x escapes to heap unnecessarily
x := 42
return &x // Escapes!
}
func main() {
ptr := unnecessaryEscape()
fmt.Println(*ptr)
}
Escape Scenarios
Scenario 1: Returning Pointers
package main
import (
"fmt"
)
type Person struct {
name string
age int
}
// Escapes: returning pointer
func createPerson(name string, age int) *Person {
p := Person{name: name, age: age}
return &p // Escapes to heap
}
// Doesn't escape: returning value
func createPersonValue(name string, age int) Person {
return Person{name: name, age: age} // Stays on stack
}
func main() {
p1 := createPerson("Alice", 30)
p2 := createPersonValue("Bob", 25)
fmt.Println(p1.name, p2.name)
}
Scenario 2: Storing in Slices
package main
import (
"fmt"
)
func storeInSlice() {
// x escapes because it's stored in slice
x := 42
slice := []*int{&x}
fmt.Println(*slice[0])
}
func main() {
storeInSlice()
}
Scenario 3: Storing in Maps
package main
import (
"fmt"
)
func storeInMap() {
// x escapes because it's stored in map
x := 42
m := map[string]*int{"answer": &x}
fmt.Println(*m["answer"])
}
func main() {
storeInMap()
}
Scenario 4: Passing to Functions
package main
import (
"fmt"
)
func processPointer(ptr *int) {
fmt.Println(*ptr)
}
func passingPointer() {
// x escapes because pointer is passed to function
x := 42
processPointer(&x)
}
func main() {
passingPointer()
}
Analyzing Escape Analysis
Use the -m flag to see escape analysis decisions.
Checking Escape Analysis
# Compile with escape analysis output
go build -gcflags="-m" main.go
# More verbose output
go build -gcflags="-m -m" main.go
# Example output:
# ./main.go:10:2: x escapes to heap
# ./main.go:15:9: createPerson ... does not escape
Example Analysis
package main
import (
"fmt"
)
type Data struct {
value int
}
func noEscape() {
d := Data{value: 42}
fmt.Println(d.value)
}
func escapes() *Data {
d := Data{value: 42}
return &d
}
func main() {
noEscape()
_ = escapes()
}
Run with: go build -gcflags="-m" main.go
Optimizing for Stack Allocation
Good: Prefer Values Over Pointers
package main
import (
"fmt"
)
type Point struct {
x, y float64
}
// Prefer returning values for small structs
func addPoints(p1, p2 Point) Point {
return Point{
x: p1.x + p2.x,
y: p1.y + p2.y,
}
}
func main() {
p1 := Point{1, 2}
p2 := Point{3, 4}
result := addPoints(p1, p2)
fmt.Println(result)
}
Bad: Unnecessary Pointer Indirection
// โ AVOID: Using pointers for small values
package main
import (
"fmt"
)
type Point struct {
x, y float64
}
// Unnecessary pointer - causes heap allocation
func addPoints(p1, p2 *Point) *Point {
result := &Point{
x: p1.x + p2.x,
y: p1.y + p2.y,
}
return result
}
func main() {
p1 := &Point{1, 2}
p2 := &Point{3, 4}
result := addPoints(p1, p2)
fmt.Println(result)
}
Interface Escape Analysis
Interfaces can cause unexpected escapes.
Good: Minimize Interface Usage
package main
import (
"fmt"
)
type Reader interface {
Read() string
}
type StringReader struct {
data string
}
func (sr StringReader) Read() string {
return sr.data
}
// Concrete type - no escape
func processStringReader(sr StringReader) string {
return sr.Read()
}
func main() {
sr := StringReader{data: "hello"}
result := processStringReader(sr)
fmt.Println(result)
}
Bad: Interface Causes Escape
// โ AVOID: Unnecessary interface usage
package main
import (
"fmt"
)
type Reader interface {
Read() string
}
type StringReader struct {
data string
}
func (sr StringReader) Read() string {
return sr.data
}
// Interface parameter causes escape
func processReader(r Reader) string {
return r.Read()
}
func main() {
sr := StringReader{data: "hello"}
result := processReader(sr) // sr escapes to heap
fmt.Println(result)
}
Closure Escape Analysis
Closures can cause variables to escape.
Good: Avoid Unnecessary Closures
package main
import (
"fmt"
)
func processWithoutClosure(values []int) {
for _, v := range values {
// Direct processing - no escape
fmt.Println(v * 2)
}
}
func main() {
values := []int{1, 2, 3}
processWithoutClosure(values)
}
Bad: Closure Causes Escape
// โ AVOID: Unnecessary closures
package main
import (
"fmt"
)
func processWithClosure(values []int) {
for _, v := range values {
// Closure captures v - causes escape
go func() {
fmt.Println(v * 2)
}()
}
}
func main() {
values := []int{1, 2, 3}
processWithClosure(values)
}
Slice Escape Analysis
Slices can cause underlying arrays to escape.
Good: Preallocate Slices
package main
import (
"fmt"
)
func buildSlice(size int) []int {
// Preallocate - stays on stack if small enough
result := make([]int, 0, size)
for i := 0; i < size; i++ {
result = append(result, i)
}
return result
}
func main() {
slice := buildSlice(10)
fmt.Println(len(slice))
}
Bad: Dynamic Slice Growth
// โ AVOID: Unbounded slice growth
package main
func buildSlice() []int {
var result []int
// Repeated allocations and escapes
for i := 0; i < 1000; i++ {
result = append(result, i)
}
return result
}
Memory Allocation Patterns
Good: Efficient Allocation Pattern
package main
import (
"fmt"
)
type Buffer struct {
data []byte
pos int
}
func NewBuffer(capacity int) *Buffer {
return &Buffer{
data: make([]byte, capacity),
pos: 0,
}
}
func (b *Buffer) Write(p []byte) {
copy(b.data[b.pos:], p)
b.pos += len(p)
}
func (b *Buffer) Bytes() []byte {
return b.data[:b.pos]
}
func main() {
buf := NewBuffer(1024)
buf.Write([]byte("hello"))
fmt.Println(string(buf.Bytes()))
}
Bad: Excessive Allocations
// โ AVOID: Creating new allocations repeatedly
package main
func processData(data []byte) []byte {
// Creates new slice each call
result := make([]byte, len(data))
copy(result, data)
return result
}
func main() {
for i := 0; i < 1000; i++ {
_ = processData([]byte("data"))
}
}
Profiling Memory Allocations
Identifying Escapes
# Build with escape analysis
go build -gcflags="-m" main.go 2>&1 | grep "escapes"
# Profile allocations
go tool pprof http://localhost:6060/debug/pprof/allocs
# Check heap profile
go tool pprof http://localhost:6060/debug/pprof/heap
Best Practices
- Prefer Stack Allocation: Keep variables on stack when possible
- Return Values: Return values instead of pointers for small types
- Minimize Pointers: Use pointers only when necessary
- Avoid Unnecessary Interfaces: Use concrete types when possible
- Profile Allocations: Use profiling to identify escape issues
- Preallocate Slices: Know the size and preallocate
- Reuse Buffers: Reuse buffers instead of creating new ones
- Understand Closures: Be aware of closure capture implications
Common Pitfalls
- Premature Optimization: Don’t optimize without profiling
- Over-Pointer Usage: Not all types need to be pointers
- Interface Overhead: Interfaces have allocation overhead
- Closure Capture: Closures can cause unexpected escapes
- Unbounded Growth: Slices that grow unbounded escape to heap
Resources
Summary
Understanding escape analysis helps you write more efficient Go code. Stack allocation is faster and doesn’t require garbage collection. Use the -m compiler flag to analyze escape decisions. Prefer returning values over pointers for small types, minimize pointer usage, and profile your code to identify allocation bottlenecks.
Comments