Skip to content
HN On Hacker News ↗

Escape Analysis in Go – Stack vs. Heap Allocations Explained - The JetBrains Blog

▲ 34 points 7 comments by ingve 3d ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is a mix of AI-generated, AI-assisted, and human-written content

46 %

AI likelihood · overall

Mixed
54% human-written 36% AI-generated
SEGMENTS · HUMAN 4 of 5
SEGMENTS · AI 1 of 5
WORD COUNT 1,823
PEAK AI % 96% · §3
Analyzed
Jul 23
backend: pangram/v3.3
Segments scanned
5 windows
avg 365 words each
Distribution
54 / 36%
human / AI fraction
Verdict
Mixed
Pangram v3.3

Article text · 1,823 words · 5 segments analyzed

Human AI-generated
§1 Human · 8%

The IDE for professional development in Go GoLand One of the design choices Google made when developing Go was to abstract memory management away from developers so they could focus on what really matters – writing code. Things like escape analysis and garbage collection are thus automatic, and the Go compiler works in almost mystical ways.

That’s one of the best features of Go, so long as your program works. But when memory issues arise, and you need to demystify the process to optimize it, that’s when the perspective shifts and the mystery is no longer so appealing.

In this article, we’ll explain one of the most confusing performance optimization problems – escape analysis, i.e. how the compiler decides what stays on the stack, and what moves to the heap. We’ll cover what escape analysis is and how it works, what the most common escape cases are and how to inspect them, why inspections might be hard to use, and even how GoLand can perhaps help with that.

What is escape analysis in Go?

Escape analysis is a compiler optimization that determines whether a value can be allocated on the stack or must be moved to the heap. In other words, in Golang, the escape analysis process inspects every value your program creates to answer the question: Can this safely live on the stack, or does something outside the current function still need it after the function returns (and therefore it needs to live on the heap)?

The stack is a per-goroutine region where allocations are significantly cheaper and reclaimed automatically when a function returns, so storage happens fast but is short-lived. The heap is a shared, longer-lived memory space that the garbage collector must track and clean up, so it’s more resource-intensive. Escape analysis is the bridge between the two.

A value is said to “escape” when the compiler can’t prove that it’s done being used by the time the function exits, as explained in the Go documentation. For each value, it asks whether any reference to that value can outlive the function that created it. If the answer is no, the value stays on the stack. If the answer is yes – or if the compiler simply can’t prove the answer is no – the value is allocated on the heap to be safe.

§2 Human · 12%

The classic example is returning a pointer to a local variable – the function ends, but a reference to that variable lives on, so the value can’t sit on the stack frame that’s about to be discarded. It escapes to the heap instead.

It’s worth pointing out here that escape decisions are not set in stone. They can change depending on how you structure your code, the Go version you’re compiling with, as well as on the environment (OS/architecture), compiler settings, and other optimization decisions like inlining. That’s why you can’t assume a value will or will not always escape in a given context – you need to check every time.

And why should I care?

Unlike in some other languages, in Go you don’t manually choose between stack and heap allocation the way you might with malloc and free in C. Instead, the compiler makes the call. Go also manages memory safety for you, so as to prevent escaped values from being unsafe.

Many developers stop there and never bother with escape analysis. After all, the documentation says that “you don’t need to know”, and if it works, it works, right?

Having said that, you do still have agency and can write code in ways that influence these compiler decisions – in a good way or indeed in a bad way. That’s why an understanding of escape analysis is actually a must-have skill for any Go developer.

Common reasons values escape to the heap

Most of the time, when a value escapes to the heap, it’s for one of a handful of recurring reasons. Recognizing these patterns helps you read compiler output faster and tells you whether a given allocation needs investigation or is simply the best way to run your code.

It’s important to note that not every escape is a problem – programs with any degree of complexity will inevitably have things living on the heap.

§3 AI · 96%

The goal here is to recognize the patterns, not to eliminate them all.

Returning pointers

Returning a pointer to a local value is probably the most common cause of an escape. The value is created inside the function, but the caller holds onto a reference after the function returns, so it can’t live on the stack frame that’s being torn down.

func NewUser(name string) *User { u := User{Name: name} // u escapes to the heap return &u }

This is safe in Go – the compiler notices that &u outlives NewUser and moves the value to the heap automatically. Whether you should care depends on context. Returning pointers is idiomatic and often the right call for API clarity and readability. The right choice depends on your API design and measured performance impact, not on a blanket rule about avoiding pointers.

Closures and goroutines

Captured variables will escape when a closure or goroutine may outlive the function that created them. The compiler has to assume the captured value is still reachable, so it allocates it on the heap.

func process(data []byte) { go func() { handle(data) // data may escape: the goroutine can outlive the process }() }

Goroutines are a frequent source of confusion here, precisely because they can keep running after the parent function has returned. From the compiler’s point of view, anything the goroutine touches might be needed indefinitely, so it plays it safe.

Interfaces and dynamic values

Passing a concrete value through an interface can sometimes lead to a heap allocation. This most often occurs in formatting, logging, and interface-based APIs, where values are boxed into an interface{} (any) before they are handled.

func logValue(v int) { fmt.Println(v) // v is passed as an interface and may escape }

However, interface use does not automatically cause heap allocation. Plenty of interface calls don’t allocate at all, and the compiler keeps getting better at this. Treat interfaces as something to check rather than avoid entirely.

Slices, maps, and structs

Values can escape when they’re stored inside a data structure that outlives the current function. If you put a pointer into a map, a slice, or a struct field, and that container lives longer than the function, the stored value has to live just as long.

§4 Human · 20%

type Cache struct { items map[string]*Item }

func (c *Cache) Add(key string, it *Item) { c.items[key] = it // it escapes: stored in a structure that outlives the call }

The relationship between the container and the value it holds is crucial here. A slice that never leaves the function may keep its contents on the stack; the same slice returned to a caller or stored in a long-lived struct will push its contents to the heap.

How to check escape analysis in Go

The good news is you don’t really need to remember any common reasons for escapes or guess whether a value escaped or not in a particular instant. The Go compiler flags can tell you that, and in fact, inspecting the compiler’s output is the only reliable way to know what’s happening for sure.

The log covers more than just escapes, though. Alongside allocation decisions, the compiler reports inlining details and other diagnostics, so you get a fairly complete picture of the optimization choices it made for a given build. The downside is that the output isn’t precisely user-friendly or easy to navigate, but we will come back to that later.

How to use compiler flags

The Go compiler surfaces escape analysis information through the -gcflags debug flag with the -m option:

go build -gcflags="-m" ./...

The -m flag asks the compiler to print its optimization decisions, including whether a value escaped. The output looks roughly like this:

./user.go:6:2: moved to heap: u ./user.go:7:9: &u escapes to heap

You can pass -m twice (-gcflags="-m -m") for more detailed reasoning, though that quickly becomes verbose. There are more flag variations, but -gcflags="-m" is the one you’ll probably reach for most.

As you can see, the output is keyed by file, line, and column, and escape analysis can be buried among other comments. This means the real work is mapping each message back to the relevant source code so you can understand it in context.

Why it’s hard to work with escape analysis logs

While compiler flags are the only way to reliably see what decisions the compiler made, they are arguably not the most ergonomic one.

§5 Human · 12%

The report may be perfectly readable when you work with a small file, but in larger projects and with daily use, it can quickly become frustrating. No wonder then that it’s a heavily underutilized feature of the Go SDK.

A few common pain points have come up in our discussions with Go developers:

The output is noisy – a real build prints escape decisions, inlining notes, and other diagnostics all in one place, and most of it isn’t what they’re looking for at the moment.

Messages are hard to connect to the source – each line is tagged with a file, line, and column, but they still have to open that file and find the right spot.

They have to constantly switch context – reading a message in the terminal, then jumping to the editor to see the code, then back again. This disrupts their concentration and slows the investigation.

Not every escaping value is worth optimizing, but the output treats every allocation equally. Meanwhile, most of them don’t matter for performance, and it’s hard to separate signal from noise.

None of this makes command-line escape analysis bad. It’s a genuinely powerful diagnostic that’s just not always convenient, especially when you’re trying to answer a focused question inside a large codebase. Because escape analysis has been locked behind obscure compiler flags and hard-to-parse logs, it’s become a niche practice even among experienced Go developers. That’s why our GoLand team has designed a tool that lowers the barrier to entry and bridges the gap between “powerful” and “convenient”.

How GoLand helps with escape analysis

The GoLand escape analysis support that arrived in the 2026.2 release was built to address the pain points we’d heard from developers. Under the hood, the tool largely does what you would do manually, running the go build command with the -gcflags="-m -m" flag. (To be precise, GoLand runs -gcflags="-m=2 -json=0,<path>", since we found that storing logs in JSON format provides a more structured and stable output).

But the tool now also adds a layer that parses that raw gcflags output and brings it directly into the editor, so you can stay close to your code while investigating allocation decisions instead of bouncing between the terminal and your files.

Running the escape analysis tool

The workflow is pretty straightforward.