TRT
Sign in
easyFoundationsconcept

How does Go's compiler decide whether a variable is allocated on the stack or the heap (Escape Analysis), why is returning a value `User` often faster than.

Explain downward pointer sharing (stack-safe) vs upward pointer returning (heap-escape), inspect `-gcflags=-m`, and trace `Unwrap()` trees in `errors.Is`/`As`.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

Two compile-and-runtime mechanics govern Go reliability and memory speed: (1) Error Wrapping (%w, Unwrap() error, and Unwrap() []error in Go 1.20+): Wrapping an error with fmt.Errorf("context: %w", err) returns an error whose .Unwrap() method returns err (and errors.Join(e1, e2) returns an error with Unwrap() []error). Never compare errors with err == target or type assertions err.(*MyErr)! errors.Is(err, target) and errors.As(err, &targetPtr) recursively walk the entire .Unwrap() tree to match sentinel values or extract typed error structs at any depth! (2) Escape Analysis (go build -gcflags="-m"): In Go, whether u := &User{} allocates on the fast goroutine stack (freed in 0 ns when the function returns) or on the garbage-collected heap is decided statically by the compiler's Escape Analysis—NOT by whether you wrote &User{} or new(User)! A variable escapes to the heap only if the compiler cannot prove its lifetime is confined to the current stack frame (for example: returning &u up the stack, storing &u in a heap slice/map, or passing u to an interface method whose concrete target isn't inlined).

errors.Is(err, target) and errors.As(err, &out) traverse the full tree of Unwrap() error and Unwrap() []error nodes; a local variable stays on the goroutine stack if and only if the compiler proves no reference to it outlives its defining stack frame. Because stack frames are reclaimed immediately when a function returns (by moving the stack pointer), any pointer returned to a caller (return &local) would dangle on a C stack; Go's compiler proves safety by moving (escaping) any such variable to the garbage-collected heap at compile time.

Allocating a struct on the goroutine stack costs 0 heap allocations and 0 GC work (~1 ns stack pointer bump), whereas escaping to the heap calls runtime.newobject (~25–40 ns allocation + later GC mark-and-sweep cost). In production systems (Go standard library errors (Is, As, Join), database/sql (sql.ErrNoRows), context.DeadlineExceeded, and cmd/compile/internal/escape (go build -gcflags="-m -m").), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how error wrapping trees (%w, errors.Is, errors.As) and compiler Escape Analysis operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Return by Value func() User (<= 64B), Return Pointer func() *User (Not Inlined)) with concrete numbers.
  • Identifies real-world failure modes (Reflexively returning *SmallStruct from every constructor (func NewPoint() *Point) thinking pointers are 'faster than copying') and explains how production systems mitigate them.

Common mistakes

  • Treating error wrapping trees (%w, errors.Is, errors.As) and compiler Escape Analysis as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Checking if err == sql.ErrNoRows or err == io.EOF directly in application code).
  • Forgetting boundary conditions and error recovery (Allocating a dynamic slice make([]byte, runtimeSize) where runtimeSize is unknown at compile time (or > 64 KB)).

This answer walks

Practice follow-ups

  1. 01Why does passing a pointer `*User` **down** into a helper function `validate(&u)` usually keep `u` on the stack, whereas returning `*User` **up** from `newUser() *User` (when not inlined) forces `u`.
  2. 02What is the correct syntax to extract a `*os.PathError` using `errors.As` (`var pe *os.PathError; if errors.As(err, &pe)`) and why must you pass `&pe` (`**os.PathError`)?

Not started

Sign in to save your learning progress.

Sign in to save