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
N/A — Go compiler/runtime concern.N/A — Go concern.N/A — Go concern.N/A — Go 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.
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
}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.
Context
This tests defer semantics — interviewers want the immediate-argument-evaluation rule and the closure workaround, which cause subtle bugs.
The mechanical answer
When a defer statement executes, the deferred call's ARGUMENTS (including the receiver for a method call) are evaluated immediately and captured; the function body runs later at return. So defer f.Close() captures the f value (the file) at that point — which is why it closes the right file even if a local variable named f is reassigned later. Similarly, defer fmt.Println(x) prints the value of x AT DEFER TIME, not at return. To defer evaluation of arguments until the call runs, wrap in a closure: defer func() { fmt.Println(x) }() — the closure captures the variable by reference, so it sees the final value. This distinction is the classic source of 'why did my deferred log print the wrong value' bugs.
Trap
The naive answer is 'defer runs the call with the values at return'. Arguments are snapshotted at defer registration. Another trap: assuming a method receiver is evaluated at return — it's evaluated immediately too (defer obj.Method() captures obj now). Candidates who explain the immediate-argument-evaluation rule and the closure fix show real understanding.