Stack Mechanics & Optimizations
Understanding Memory: Stack vs. Heap in Go
To write high-performance Go, you must understand where your variables live. Go abstracts memory management, but it doesn’t eliminate the physics of hardware.
The Two Worlds
1. The Stack
- What it is: A pre-allocated, contiguous block of memory for each goroutine (starts at 2KB).
- Allocation: Instant (move a pointer).
- Deallocation: Instant (move a pointer back).
- GC Cost: Zero. The Garbage Collector (GC) does not scan the stack.
- Cache Locality: Excellent.
2. The Heap
- What it is: A global pool of memory for dynamic allocations.
- Allocation: Slow (search for free block, possibly lock).
- Deallocation: Garbage Collected (expensive scan).
- GC Cost: High. Pointers on the heap must be traced.
- Cache Locality: Poor (scattered).
Go’s Optimization Goal
Keep everything on the stack.
If the compiler can prove a variable’s life cycle is contained within a function (it doesn’t “escape”), it allocates it on the stack.
Stack Growth (Contiguous Stacks)
Goroutines start with 2KB stacks. If a function call needs more space, the runtime: 1. Allocates a larger stack (e.g., 4KB). 2. Copies everything from old stack to new stack. 3. Updates pointers to the new stack. 4. Frees the old stack.
Note: This “copying” is why pointers to stack variables are safe only if they don’t escape. If they escaped to the heap, moving the stack would invalidate external pointers.
2026: The “Mid-Stack” Inlining
Recent Go versions have become aggressive about inlining function calls mid-stack. * If UserFunc calls AllocFunc, and AllocFunc is small, the compiler copies AllocFunc’s body into UserFunc. * This removes the function call overhead and allows variables that might have escaped (due to being return values) to stay on the stack.
Visualization
| Variable Scenario | Destination | Why? |
|---|---|---|
x := 42 |
Stack | Never leaves function references. |
y := &x (used locally) |
Stack | Address taken, but scope is local. |
return &x |
Heap | Escapes to caller; stack frame dies on return. |
fmt.Println(x) |
Heap (usually) | fmt.Println takes interface{}, which often causes escape. |
make([]byte, 1024) |
Stack | Size known at compile time, fits in stack. |
make([]byte, n) |
Heap | Size unknown at compile time. |
Practical Rule
Don’t fear the Heap, but respect the Stack. If you are writing a hot loop (a game loop, a high-frequency trading handler), ensure your temporary variables do not escape to the heap.
Use detailed analysis tools to verify this (covered in next chapter).