Memory in Go
Go has no new and delete in the C++ sense — it has a garbage collector, and that creates the illusion that data placement needs no thought. The illusion is dangerous: where a value actually lives — in a frame's stack or in the GC-managed heap — is decided by the compiler at compile time, and that decision directly drives how much work the collector inherits. An unnecessary escape to the heap will not break the program, but it turns a cheap local into a load on the GC.
This topic's traps cluster around one misconception — that placement is decided by the keyword (var, new, make). In reality escape analysis picks it from how the value is used: a pointer to a local often stays on the stack, while a plain var escapes to the heap if its address leaks. On top of that there are cases where a value lands on the heap beyond ordinary escape — unknown size, too-large value, interface conversion. And the upshot of the layout shows up in speed: a contiguous slice beats a linked list not because of asymptotics but because of cache locality. This topic dissects Go memory layer by layer — from the layout of a goroutine stack to hardware prefetch.
Topic Map
- Stack vs heap — each goroutine has its own small growable stack, the GC-managed heap, and the compiler — not the keyword — decides where a value lives.
- Escape analysis — a compile-time pass that decides whether a value outlives its frame and therefore escapes to the heap.
- Forced heap allocation — cases beyond ordinary escape: unknown size, too-large value, interface conversion.
- new and make —
new(T)gives a zeroed*Tfor any type;makeinitializes a slice, map, or channel and returns a ready value, not a pointer. - Cache locality — a contiguous slice beats a linked list on sequential traversal thanks to prefetching and rare cache misses.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Believing the keyword decides stack vs heap rather than escape analysis | Wrong allocation model — cannot read a -gcflags=-m profile |
| Thinking a goroutine stack has a fixed size | Cannot explain why a goroutine is cheap or why deep recursion does not fault instantly |
| Assuming pointer types always live on the heap | Wrong placement guess — a pointer to a local often stays on the stack |
| Calling escape analysis a runtime mechanism | Conceptual miss — it is a static, compile-time analysis |
| Thinking taking the address of a local always triggers an escape | An escape happens only if the address outlives the frame — otherwise the value stays on the stack |
| Thinking escape analysis is the only thing that puts a value on the heap | Unknown size, an over-large value, and interface conversion are forced to the heap |
Thinking a make with a variable length can stay on the stack | Wrong allocation estimate — an unknown size is forced to the heap |
Believing make returns a pointer | Only new returns a pointer; make returns the slice/map/chan value itself |
Applying new to a map or channel | new(map[K]V) gives a *map over a nil map — writing to it panics; use make for a working map |
Believing equal O(n) promises equal speed | On a sequential traversal a slice beats a list due to locality and prefetch, not asymptotics |
Storing hot data as []*T for "cheapness" | The extra indirection scatters objects across the heap and kills hardware prefetch |
Interview Relevance
Memory is a mandatory topic at any serious Go interview. The interviewer checks not whether you know the word "heap", but whether you have a working model of what the compiler decides versus what the runtime decides — and where it hits performance.
What interviewers usually check:
- The difference between stack and heap — who frees each region and when, who picks the location.
- What escape analysis is — a static compiler pass, not runtime observation.
- Why the value's type alone does not decide placement — a pointer to a local often stays on the stack.
- Which cases send a value to the heap beyond ordinary escape — unknown size, excessive size, interface conversion.
- How
newdiffers frommake— a zeroed pointer versus a ready structure, and why neither controls placement. - Why a contiguous
slicebeats a linked list on sequential traversal — cache lines and hardware prefetch.
A typical wrong answer: "pointer types always live on the heap." This opens a discussion that it is not the type but escape analysis that decides: a pointer to a local that never leaves its frame stays on the stack, and the placement is visible with the -gcflags=-m flag.