skip to content

In Go's go build -gcflags=-m output, what is the difference between “moved to heap: x” and “x escapes to heap”?

level: middleimportance: should knowfreq 46%

answer

  1. one is about a variable, one about a value
  2. named local versus expression result
  3. also read leaking param and does not escape
  4. a second -m prints the reasoning

basics

~20 s

“moved to heap: x” names a variable whose address escapes, so the variable itself is now heap-allocated. “x escapes to heap” refers to the value an expression produces — a composite literal, a make, a boxed argument — that must be heap-allocated.

solid answer

~50 s

Both lines are escape-analysis diagnostics from the compiler, but they talk about different things. `moved to heap: x` is about a *named* variable, local or parameter, whose address is taken and outlives the frame; the variable is promoted to the heap and the frame keeps a pointer to it. `x escapes to heap` is about the *value* an expression yields — `&Config{...}`, a `make`, a value boxed into an interface — which cannot be stack-allocated. Two more lines matter as much: `leaking param: p` means the callee retains the pointer beyond the call, so callers must assume their argument escapes, while `p does not escape` is the opposite promise and lets callers keep the argument in their own frame. Run it as `go build -gcflags=-m ./...`; a second `-m` adds the reasoning chain that led to each decision.

code

go · 10 lines
go
type Config struct{ Name string }

var current *Config

func setCurrent(c *Config) { current = c }

func Load(name string) {
	c := Config{Name: name}
	setCurrent(&c)
}

go deeper

for a junior

Know that the compiler will tell you where things are allocated if you ask, with go build -gcflags=-m, and that the lines mentioning heap are the ones about allocation.

for a middle

Be able to read the four common lines and say which is about a declared variable, which about an expression's value, and which two describe what a callee does with a pointer parameter.

for a senior

Demonstrate the workflow: narrow the build to one package, read the callee contracts first, then use a second -m to explain a decision, and confirm any change with a benchmark rather than with the output alone.

for a principal

Decide how much of this belongs in the team's routine. Escape output is a debugging aid, not a gate: pinning it into CI encodes compiler behaviour that legitimately changes between releases, and allocation budgets belong on benchmarks instead.

## Where the output comes from Escape analysis is a compiler pass, so its findings are build-time output, not something a running program reports. `go build -gcflags=-m ./...` asks the compiler to narrate its decisions; `-gcflags='-m -m'` (equivalently `-gcflags=-m=2`) raises the verbosity so each decision is followed by the chain of reasoning that produced it. By default `-gcflags` applies only to the packages named on the command line — use `-gcflags=all=-m` when you also want the decisions inside dependencies. The output is a stream of `file:line:col: message` lines. Four messages carry almost all the information. ## moved to heap: x This is about a **named variable** — a local or a parameter — whose address is taken and escapes. The compiler cannot keep it in the frame, so the variable itself is allocated on the heap and the frame holds a pointer to it. Every read and write of `x` in that function becomes an indirection. You see it for the classic `n := 0; return &n`, and for a local whose address is handed to something that stores it. ## x escapes to heap This is about a **value produced by an expression**, not about a declared variable. Typical subjects are `&Config{...}`, `make([]byte, n)`, a string conversion, or a value boxed into an interface. The compiler is reporting that the storage backing that expression must come from the heap. The distinction is easy to see in one function: `c := Config{}` followed by something that escapes `&c` produces `moved to heap: c`, whereas `return &Config{}` produces `&Config{} escapes to heap` — there is no named variable to move, only an allocation to place. ## leaking param: p This one is about the **contract a function offers its callers**, and it is the most useful line in the output. `leaking param: p` says: this function retains the pointer `p` beyond its own return — it stores it in a package-level variable, in a struct that escapes, in a channel, or hands it to something else that leaks it. Because of that, every caller that passes the address of a local must assume that local escapes. A weaker variant reads `leaking param: p to result ~r0 level=0`. That means the pointer only flows out through a return value. The caller is not automatically forced onto the heap: if the caller does not let the result escape either, the original can still live in a frame. ## p does not escape The promise you want on a hot-path helper. It says the callee only uses the pointer during the call. Callers can pass `&local` freely and keep `local` in their own frame. This is why passing pointers around is not, in itself, an allocation: whether it costs anything depends on this line in the callee. ## Reading it in practice The output is verbose because it reports every decision, including all the boring ones. A workable routine is: 1. Build the one package you care about, not the whole tree, so the output stays small. 2. Grep for the function you are looking at. 3. Read the `leaking param` and `does not escape` lines of everything it calls — those decide whether your locals can stay put. 4. Only then look at your own `moved to heap` and `escapes to heap` lines, and turn on the second `-m` to see *why* each one happened. ## Two cautions First, the analysis is conservative by construction. When the compiler cannot see the callee — an indirect call through an interface or a function value, for instance — it must assume the pointer leaks. A `moved to heap` line therefore means *the compiler could not prove otherwise*, not *this value provably outlives the frame*. Second, these messages are diagnostics, not a stable interface. Both the wording and the decisions change between compiler releases, and a newer compiler regularly removes an allocation an older one made. Use the output to understand and to guide a change, and use a benchmark to confirm the change actually mattered. ## Rule of thumb `moved to heap` = a variable you declared, now living on the heap. `escapes to heap` = an allocation the compiler had to place on the heap. `leaking param` = a callee that keeps your pointer. `does not escape` = a callee that gives it back.

  • How do you get escape-analysis output for dependency packages too, not just the one you are building?
    Prefix the flag value with a package pattern: `go build -gcflags=all=-m ./...` applies `-m` to every package in the build, including the standard library. Without a pattern, `-gcflags=-m` applies only to the packages named on the command line, which is usually what you want because the full output is enormous.
  • What does “p does not escape” on a parameter tell the caller?
    That the callee only uses the pointer during the call and never retains it. The caller can therefore pass the address of a local and keep that local in its own frame. It is the line to look for when you are trying to explain why a helper you wrote allocates and a similar one does not.
  • What does the second -m add?
    Verbosity: alongside each decision the compiler prints the chain that produced it — how a value flowed into the thing that made it escape, step by step. On a decision you do not understand, that chain usually names the exact assignment, call or return responsible, which the single -m form leaves implicit.

saying these in an interview costs you the question

  • Treats the two messages as interchangeable wording
  • Thinks -m output is produced by the running program
  • Reads leaking param as a memory leak bug
  • Assumes the messages are guaranteed stable across releases
  • Ignores the callee lines that decide the caller's placement