Memory in Go
Stack versus heap allocation, escape analysis, forced heap allocation, new versus make, and cache locality.
8 questions
JuniorTheoryVery commonWhat is the difference between stack and heap allocation in Go?
What is the difference between stack and heap allocation in Go?
Each goroutine has its own small, growable stack for short-lived locals, freed automatically when the frame returns. The heap holds values that outlive their frame and is reclaimed by the garbage collector. The compiler, not the programmer, picks where a value lives.
Common mistakes
- ✗Believing the keyword (
var,new,make) decides stack vs heap rather than the compiler's escape analysis - ✗Thinking a goroutine stack has a fixed size — it starts small and grows on demand
- ✗Assuming pointer types always live on the heap
Follow-up questions
- →How does the runtime grow a goroutine stack when it runs out of space?
- →Why is heap allocation more expensive than stack allocation in Go?
JuniorTheoryCommonHow do you view the compiler's escape-analysis decisions in Go?
How do you view the compiler's escape-analysis decisions in Go?
Pass -gcflags=-m to go build or go run: go build -gcflags=-m ./... prints each decision, such as moved to heap: x or &x escapes to heap. Repeat it (-gcflags='-m -m') for the reasoning behind a decision. No code changes are needed.
Common mistakes
- ✗Looking for a runtime flag when escape analysis is a compile-time decision printed by the compiler
- ✗Confusing a heap profiler's allocation report with the compiler's per-variable escape decisions
- ✗Forgetting
./...or a package path, so the flag applies to nothing
Follow-up questions
- →What does a second
-m(-gcflags='-m -m') add to the output? - →Why might a variable you expected on the stack show
escapes to heap?
JuniorTheoryCommonWhat is the difference between the built-in new and make in Go?
What is the difference between the built-in new and make in Go?
new(T) allocates zeroed storage for any type T and returns a pointer *T to its zero value. make is only for slices, maps, and channels: it initializes the internal structure and returns a ready value of T itself, not a pointer.
Common mistakes
- ✗Thinking
new([]int)gives a usable slice — it returns a*[]intpointing at a nil slice; usemake([]int, 0)instead - ✗Believing
makereturns a pointer rather than the initialized value of type T itself - ✗Believing the keyword decides stack vs heap rather than the compiler's escape analysis
Follow-up questions
- →Why is
new(map[string]int)not usable for writes? - →What actually decides whether a
new-allocated value lands on the stack or the heap?
MiddleTheoryCommonWhat is escape analysis and how does the compiler decide stack vs heap?
What is escape analysis and how does the compiler decide stack vs heap?
Escape analysis is a compile-time pass that decides where a value lives. If the compiler proves the value's lifetime stays within its frame, it goes on the stack. If the lifetime may outlive the frame — the address is returned, stored in a heap object, or captured by an escaping closure — it escapes to the heap.
Common mistakes
- ✗Calling escape analysis a runtime mechanism — it is a static, compile-time analysis
- ✗Believing taking a local's address always forces a heap allocation — it only escapes if the address outlives the frame
- ✗Thinking the type alone (pointer vs value) decides escape rather than how the value is used
Follow-up questions
- →How can
go build -gcflags='-m'show you a value's escape decision? - →Why can returning a pointer to a local be safe in Go but undefined behavior in C?
MiddlePerformanceOccasionalFor sequential traversal, why does a contiguous slice usually beat a linked list?
For sequential traversal, why does a contiguous slice usually beat a linked list?
A slice stores its elements contiguously, so a traversal walks one cache-friendly memory block — the CPU prefetcher loads the next elements ahead of time and cache misses are rare (spatial locality). A linked list scatters nodes across the heap, so each next is a pointer chase to an unpredictable address, causing frequent cache misses and stalls.
Common mistakes
- ✗Reasoning only about big-O and ignoring constant-factor cache effects
- ✗Thinking pointer-chasing in a list is as cache-friendly as a contiguous scan
- ✗Attributing the gap to GC or bounds checks rather than memory layout
Follow-up questions
- →What is a cache line, and how does it explain the prefetcher's advantage on a slice?
- →When would a linked list still be the right choice despite the traversal cost?
MiddleTheoryOccasionalWhen should a Go function return a value, and when should it return a pointer?
When should a Go function return a value, and when should it return a pointer?
Return small, simple structs by value: the copy is cheap, the value can stay on the stack, and it adds no GC pressure. Return a pointer when the struct is large (copying is costly), when callers must share and mutate one instance, or when nil is a meaningful result. A pointer often forces a heap escape, so the value adds GC work.
Common mistakes
- ✗Reflexively returning pointers everywhere, adding needless heap escapes and GC work
- ✗Thinking value returns are only for primitives, not small structs
- ✗Ignoring that returning a pointer to a local usually forces a heap allocation
Follow-up questions
- →How would
go build -gcflags=-mshow you whether a returned value escaped to the heap? - →Why does returning
nilas a sentinel push an API toward pointer return types?
SeniorTheoryOccasionalBesides ordinary escape, what cases force a value onto the heap in Go?
Besides ordinary escape, what cases force a value onto the heap in Go?
Beyond ordinary escape, the compiler heap-allocates when the size is unknown at compile time — make with a variable length, or append growing a backing array — when a value is too large for the stack, and when it escapes through an interface conversion or reflection.
Common mistakes
- ✗Thinking escape analysis is the only thing that ever puts a value on the heap
- ✗Believing a
makewith a runtime-variable length can still be stack-allocated - ✗Assuming a value never heap-escapes when converted to an interface
Follow-up questions
- →Why does converting a small value to an
interface{}often cause a heap allocation? - →How does the compiler choose the stack-size threshold above which a value must escape?
SeniorTheoryRareIs reading a value from the stack actually faster than reading it from the heap in Go?
Is reading a value from the stack actually faster than reading it from the heap in Go?
Not the load itself: once an address is in a register, reading costs the same wherever the value lives — the CPU cannot tell stack from heap. Stack allocation wins elsewhere: no GC scanning or collection, near-free alloc/free by bumping the stack pointer, and better cache locality from hot, contiguous frames. The gain is about allocation and GC pressure, not per-read latency.
Common mistakes
- ✗Believing the CPU reads stack memory with faster instructions than heap memory
- ✗Attributing the stack's speed to per-read latency rather than to allocation cost and GC pressure
- ✗Thinking a heap read pays a garbage-collector lock or fence on every access
Follow-up questions
- →Why does escaping to the heap hurt throughput even when each individual access is equally fast?
- →How does the cache locality of contiguous stack frames affect real-world read performance?