The Go Runtime
Most languages hide the moment where "value" and "reference" diverge. Go does not: it passes everything by value, evaluates defer's arguments at one point and runs the body at another, and captures variables in a closure by reference. Each rule on its own is simple — but it is at their seams that the bugs are born which, in an interview, separate those who "have written Go" from those who understand its execution model.
Every trap in this topic is about the moment and about what exactly is copied. defer queues a call for the function's return (not a block's, not a loop iteration's), but evaluates that call's arguments immediately, at the defer point. A closure holds the variable itself, so a counter "lives on" between calls rather than resetting. A method is syntactic sugar for passing the receiver as an argument, so a value receiver gets a copy and the mutation is lost. And reassigning a pointer parameter changes only the local copy of the pointer. This topic dissects the runtime layer by layer — from closures to inlining.
Topic Map
- Closures — a function value captures enclosing variables by reference, not a copy, and they live as long as the function does.
- Defer mechanics —
deferqueues a call for the function's return; multipledefers run in LIFO order. - Defer argument evaluation — a deferred call's arguments are evaluated at the
deferpoint, not at exit; wrapping in a closure defers evaluation too. - Defer in a loop —
deferhoards calls until the function returns, not until the iteration ends — in a loop that is a descriptor leak. - Function inlining — the compiler pastes the callee's body into the call site, removing call overhead and opening the way to further optimizations.
- Pass by value — Go always copies the argument, including pointers; reassigning a pointer parameter does not change the caller's pointer.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
| Thinking a closure copies the variable | Capture is by reference — all closures see one variable |
Believing defer fires at the end of a block or iteration | defer runs at the function's return, not a block's |
Thinking defer arguments are evaluated at exit | They are snapshotted at the defer point; a later change has no effect |
Putting defer f.Close() in a loop | Closes pile up until the function ends — a descriptor leak |
| Believing Go passes structs by reference | Go always copies the argument; only a pointer gives a reference |
| Reassigning a pointer parameter and expecting a caller-side effect | Only the local copy of the pointer changes |
| Mutating fields through a value receiver | The method edits a copy; the change is invisible outside |
| Assuming any function gets inlined | Size and certain constructs limit inlining |
Interview Relevance
The runtime is a frequent source of "what does this code print" in Go interviews. The interviewer checks not memorization but a precise model: what is copied, when it is evaluated, and where a variable lives.
What interviewers usually check:
- How a closure relates to enclosing variables — capture by reference, not a copy, and why a counter persists between calls.
- What
deferdoes and in what order multiple deferred calls run. - When
deferarguments are evaluated and how wrapping in a closure changes it. - Why
defer f.Close()in a loop is a bug and how to correctly scope the cleanup. - Why Go is always pass-by-value, and what follows for pointer parameters and value receivers.
A typical wrong answer: "defer runs at the end of the block, and its arguments are evaluated at exit." That opens a discussion of how defer fires at the function's return while arguments are snapshotted at the defer point — two different moments, and it is at their seam that most of this topic's traps live.