Right, let’s talk about real estate. Not the kind with open houses and questionable wallpaper, but the kind your program cares about: where your values live. This isn’t just academic; it dictates who cleans up the mess, how fast things are, and whether you get a segfault at 2 AM.

I need you to forget the “stack is fast, heap is slow” mantra for a second. It’s a symptom, not the cause. The real question is: why does the language put a value in one place or the other? The answer, in Go, isn’t you. At least, not entirely. You make requests with your code’s structure, and the compiler makes the final call using a brilliant process called escape analysis.

The Golden Rule of Thumb

Here’s the single most important concept to grip: If the compiler can prove a value is not used after the function that created it returns, it will allocate that value on the stack. Otherwise, it must escape to the heap.

The stack is a per-function scratchpad. When a function (main, calculate, whatever) is called, it gets a block of memory (a “stack frame”). All the local variables for that function live there. When the function returns, poof—the entire stack frame is invalidated and reused for the next function call. It’s incredibly fast because it’s just a pointer move. But it also means you cannot safely hold a reference to something on a stack frame that’s gone. That’s like writing your friend’s address on a napkin, then watching them move out and a stranger move in. mailing that napkin is going to lead to confusion.

The heap, on the other hand, is the shared, long-term storage pool. Values there exist until the garbage collector determines nothing needs them anymore. Accessing it is slower because finding a spot, allocating it, and later collecting it is more complex. But it’s necessary for anything that needs to outlive its creator function.

Let’s see this in action. The compiler decides this by looking at how references to a value escape its function.

func getInt() *int {
    // This v lives INSIDE getInt's stack frame.
    v := 42
    // We return the address of this local variable.
    // This is the moment the compiler's alarm bells go off.
    // "If I put 'v' on the stack, the address I return will point to
    // invalid memory the moment getInt returns. That's bad."
    // So, the compiler makes v "escape" to the heap.
    return &v
}

func main() {
    myValue := getInt() // myValue points to a value on the heap.
    fmt.Println(*myValue) // This is perfectly safe.
}

Run this with go build -gcflags="-m" to see the compiler’s reasoning. You’ll get a line saying something like moved to heap: v. The compiler saw the reference escaping (via the return statement) and made the only sane choice.

When the Stack is Safe

Now, the opposite. Most of the time, things don’t escape. And that’s good!

func calculateTax(cost int) int {
    // These are local calculations. Their addresses are never taken
    // and never shared outside this function.
    rate := 0.08
    tax := float64(cost) * rate
    return int(tax) // Just the value is returned, not an address.
}

Here, rate and tax are prime candidates for the stack. They are born, used, and die within calculateTax’s stack frame. No references escape. The compiler doesn’t even break a sweat; it slams these onto the stack for lightning-fast allocation and cleanup.

Why You Should Care (Besides Speed)

Understanding escape analysis isn’t just for performance pedants. It demystifies behavior that often trips people up.

The Slice Header Trap: This is a classic. A slice is a struct value (a header) containing a pointer, length, and capacity. The header itself can be on the stack, but the underlying array it points to is a different story.

func getSlice() []int {
    data := make([]int, 10) // The []int header might be on the stack...
    data[0] = 1
    // ...but the underlying array (pointed to by 'data') must be on the heap.
    // Why? Because we are returning the slice header, which contains a pointer
    // to that array. The array itself must outlive this function call.
    return data
}

Even though it looks like we’re just returning a value, the compiler is smart enough to see that the contents of that value are a pointer to something that must escape. So make will allocate the array on the heap in this scenario.

Function Literals (Closures): Another big escape hatch. If you close over a variable from an outer function, that variable must live at least as long as the closure itself, which usually means the heap.

func counter() func() int {
    i := 0 // i escapes! It must live for the lifetime of the returned function.
    return func() int {
        i++
        return i
    }
}

The anonymous function needs access to i long after counter() has returned. The compiler knows this, so it promotes i to the heap. The closure holds a reference to that heap location.

The Takeaway: Write Clear Code, Let the Compiler Work

Your job isn’t to micromanage stack vs. heap. Trying to outsmart the compiler is a fool’s errand. Your job is to write clear, idiomatic code. Use value semantics where appropriate. Pass pointers when you need to mutate shared state or for large structs. But mostly, trust the escape analysis.

It’s phenomenally good. If a value can be on the stack, it will be. If it needs to be on the heap, it will be. This is the magic of a modern compiled language: you get memory safety and performance without having to manually manage every single byte. You focus on the problem, and the compiler focuses on the real estate.