Under The Hood
backend go Go 1.21+ (open-coded defers)

defer Evolution: Open-Coded vs Stack-Allocated vs Heap-Allocated

Last updated
Prerequisites:
Go function basics and defer usage
Understanding of compiler optimization and function frames
  • go
  • defer
  • compiler
  • stack
  • heap
  • open-coded

Read at your depth

The practical view

defer registers a function call to run when the current function returns: defer file.Close(). Defers run in LIFO order (last registered runs first). The arguments are evaluated immediately, the call runs at return. Historically, defer had a small cost (heap allocation in early Go). Modern Go (1.14+) uses open-coded defers for simple cases: the deferred calls are inlined at the end of the function with no allocation. This made defer essentially free in the common case. Use defer for cleanup (close files, unlock mutexes, recover) — it's now idiomatic and cheap.

The same idea in other frameworks

react equivalent
N/A — Go compiler/runtime concern.

Legacy vs modern

Manual cleanup vs defer cleanup

Manually remembering to close/unlock on every return path is error-prone; defer guarantees cleanup runs on ALL exits, and modern Go makes it free.

before → after
Manual cleanup on every return
func read(path string) ([]byte, error) {
  f, err := os.Open(path)
  if err != nil { return nil, err }
  data, err := io.ReadAll(f)
  f.Close() // ❌ OLD: manual close — easy to forget on an early return
  return data, err
}
defer cleanup (open-coded)
func read(path string) ([]byte, error) {
  f, err := os.Open(path)
  if err != nil { return nil, err }
  defer f.Close() // ✅ NEW: guaranteed on every return, ~free in modern Go
  return io.ReadAll(f)
}

Interview gotchas

Context

This tests the compiler's open-coding decision — interviewers want the limits (loop defers, recover, count) that force the fallback to runtime _defer records, which is where defer cost returns.

The mechanical answer

Open-coded defers only apply when the compiler can statically prove the set of deferred calls at compile time: typically ≤8 defers per function, none inside a loop, no recover() in the same function, and no pathological function size. When a defer is inside a LOOP, the number of defer registrations is dynamic, so the compiler cannot inline them — it falls back to creating a runtime _defer record per iteration, which (since Go 1.13) is stack-allocated when possible but can still escape to the heap (e.g. when the defer is in a loop that runs many times, the records accumulate until the function returns). recover() also forces the runtime path because it needs the defer chain. So defer in a hot loop is the case where the 'free defer' promise breaks.

Trap

The naive answer is 'defer is always free in modern Go'. Open-coded defers cover the common case; loop defers and recover still allocate _defer records. Another trap: thinking the allocation is per function — it's per defer REGISTRATION, so a defer inside a loop allocates per iteration. Candidates who enumerate the open-coding limits show real compiler understanding.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.