skip to content

In Go, is it safe to return a pointer to a local variable from a function?

level: juniorimportance: must knowfreq 72%

answer

  1. the compiler picks the location, not you
  2. Go has no dangling pointers
  3. the address outlives the frame
  4. moved to heap: n

basics

~20 s

Yes. Go has no dangling pointers: at compile time escape analysis sees that the address outlives the function's frame, so the compiler allocates that variable on the heap instead of the stack. The garbage collector frees it later.

solid answer

~40 s

It is always safe. Go's compiler runs escape analysis on every function: it tries to prove that a value's lifetime ends when the frame does, and if it cannot, the value is allocated on the heap instead. So `n := 0; return &n` is legal and correct — the compiler quietly promotes `n` to a heap allocation, and the garbage collector reclaims it once no reference remains. You can see the decision with `go build -gcflags=-m`, which prints `moved to heap: n` for exactly this case. The important consequence is that placement is a compiler decision, not something the syntax controls: `new(T)` and `&T{}` are treated identically, and plenty of pointers never touch the heap at all. Escaping is a performance question — an allocation plus garbage-collector work — never a correctness one.

code

go · 4 lines
go
func newCounter() *int {
	n := 0
	return &n // safe: n is heap-allocated, not a dangling pointer
}

go deeper

for a junior

Be ready to say plainly that returning the address of a local is safe in Go, and that the compiler moves that variable to the heap for you. Knowing the phrase escape analysis and that it happens at compile time is enough here.

for a middle

Explain the mechanics: the stack is the default, a value escapes when the compiler cannot prove its lifetime ends with the frame, and go build -gcflags=-m shows the decision. Be able to list the shapes that cause an escape.

for a senior

Show that you treat escaping as a cost, not a bug: you leave it alone until a benchmark says otherwise, then reshape the API so callers own the memory. Be clear that escape decisions are compiler behaviour, not a specification guarantee.

for a principal

Own the standard your codebase applies: hot paths where allocation counts are tracked deliberately, and everywhere else pointers used because they read best. Push back on style rules that ban pointer returns in the name of performance nobody measured.

## The question behind the question Anyone arriving from C asks this first, because in C returning `&local` hands back a pointer into a frame that has already been popped, and using it is undefined behaviour. Anyone arriving from Java or Python asks the opposite question, because there every object is on the heap and there is nothing to worry about. Go sits between the two, and the answer is: write the code that reads best, and the compiler decides where the memory lives. ## Stack first, heap when it must Every Go function call gets a frame on the calling goroutine's stack. Local variables live there by default, which is the cheapest possible allocation — bumping a pointer, and no cleanup at all, because the whole frame disappears when the function returns. That only works while a value's lifetime is contained by the frame. If a reference to a local can still be reached after the function returns, the frame is the wrong place for it. The compiler detects this case during a pass called **escape analysis**: a value *escapes* when the compiler cannot prove its lifetime ends with the frame. Escaped values are allocated on the heap and are the garbage collector's responsibility. ## The canonical example ```go func newCounter() *int { n := 0 return &n } ``` The address of `n` is returned, so `n` outlives `newCounter`. Escape analysis notices, and `n` is heap-allocated. The frame holds a pointer to it. Callers get a perfectly valid `*int`. Build with `go build -gcflags=-m` and the compiler tells you so, printing `moved to heap: n`. Contrast: ```go func sum(a, b int) int { t := a + b return t } ``` Nothing takes the address of `t`, so `t` lives in a register or the frame and costs nothing. ## Why placement is not visible in the syntax The biggest misconception here is that some spelling forces the heap. It does not: - `new(T)` and `&T{}` are equivalent. Both produce a `*T`, and both are stack-allocated if the pointer does not escape. - Passing a pointer into a function does **not** force a heap allocation. If the callee only reads through the pointer and does not store it anywhere that outlives the call, the pointed-to variable stays in the caller's frame. Escape analysis records, per function, whether a pointer parameter is retained. - Not taking a pointer does not guarantee the stack either. A large local, or a `make` whose size is not a compile-time constant, is heap-allocated because the compiler cannot bound the frame. ## What escaping actually costs The program is correct either way — this is the point worth internalising. What changes is cost: a heap allocation is more expensive than a frame slot, and every heap object adds work for the garbage collector. In ordinary code that is irrelevant. In a function called millions of times a second it is exactly the thing you profile and then try to remove. That is also why the escape decision is not part of the language specification. It is a property of the current compiler, and it improves between releases. Code that relies on a value staying on the stack for *correctness* would be broken code; there is no such dependency in Go, because the semantics are identical either way. ## What typically makes a value escape At this level it is enough to recognise the shapes: - Its address is returned, as above. - Its address is stored somewhere longer-lived: a package-level variable, a field of an already-escaping struct, a slice or map that escapes. - It is captured by a function literal that itself escapes — stored in a slice, returned, or started as a goroutine. - It is passed to a function the compiler cannot see through, so the compiler must assume the worst. - It is too large, or its size is not known at compile time. ## How to check `go build -gcflags=-m ./...` prints one line per decision: `moved to heap: n` for a named variable whose address escapes, `&T{} escapes to heap` for an escaping composite literal, and `p does not escape` for a pointer parameter the callee does not retain. Adding a second `-m` (`-gcflags='-m -m'`) also prints the reasoning that led to each decision. ## Takeaway Return the pointer if the API wants a pointer. Go guarantees it is valid. Then, if and only if a benchmark says the allocation matters, go look at what the compiler decided and whether the API can be shaped to avoid it.

  • Does new(T) allocate on the heap while &T{} allocates on the stack?
    No. They are two spellings of the same thing, and neither decides placement. Escape analysis does: if the resulting pointer does not outlive the frame, either form stays on the stack; if it escapes, either form is heap-allocated. The only difference is that a composite literal can also set field values.
  • If a local escapes to the heap, is the program still correct?
    Yes — escaping is purely a cost question. The semantics are identical: the value lives as long as anything references it, and the garbage collector reclaims it afterwards. What changes is that you paid for an allocation and added an object the collector must trace, which only matters on a hot path.
  • Would returning the value instead of a pointer avoid the heap?
    Usually yes, for a small struct. Returning by value copies into the caller's frame, so nothing needs to outlive the callee. It stops being a win for large structs, where the copy costs more than the allocation, and it is not available when callers must observe mutations through a shared pointer.

It is like a coat check that notices you are leaving with the coat: if the item is walking out of the building, it does not get stored in the cloakroom that closes with the room.

saying these in an interview costs you the question

  • Says the pointer dangles once the function returns
  • Claims new allocates on the heap and & on the stack
  • Thinks the runtime decides placement at call time
  • Believes passing any pointer forces a heap allocation
  • Assumes all local variables in Go are stack-allocated