The Go Runtime
Go runtime semantics — defer, panic/recover, initialization order, language subtleties.
9 questions
JuniorTheoryCommonWhat is a closure in Go, and how does it relate to variables in its enclosing scope?
What is a closure in Go, and how does it relate to variables in its enclosing scope?
A closure is a function value that captures variables from its enclosing scope by reference — it refers to the real variable, not a copy. It can read and modify them, and they live as long as the closure does, even after the enclosing function returns.
Open full question →Common mistakes
- ✗Believing the closure captures variables by value, taking a snapshot at creation instead of referencing the live variable
- ✗Assuming a captured local dies when the enclosing function returns, rather than living as long as the closure
- ✗Confusing a closure with a plain anonymous function that has no access to the enclosing scope
Follow-up questions
- →How does Go keep a captured variable alive after its function returns?
- →What happens if two closures capture the same enclosing variable?
JuniorCodeCommonWhat does defer do, and in what order do multiple deferred calls run?
What does defer do, and in what order do multiple deferred calls run?
defer schedules a call to run when the surrounding function returns. Multiple deferred calls run in LIFO order — last registered, first executed — so they print in reverse of registration order.
Common mistakes
- ✗Expecting deferred calls to run in registration (FIFO) order rather than LIFO
- ✗Thinking
deferruns the call right away instead of at function return - ✗Believing the deferred call is skipped when the function panics
Follow-up questions
- →Does a deferred call still run if the function panics?
- →Where are pending deferred calls stored for a single goroutine?
MiddleDebuggingCommonWhy is defer f.Close() inside a loop a bug, and how do you scope the cleanup correctly?
Why is defer f.Close() inside a loop a bug, and how do you scope the cleanup correctly?
defer runs at function return, not per iteration, so every f.Close() is queued and nothing closes until the function exits — with many files you exhaust file descriptors. The same trap with defer mu.Unlock() self-deadlocks on the next iteration. Fix: move the loop body into a helper that returns each time.
Common mistakes
- ✗Thinking
deferruns at the end of each loop iteration rather than at function return - ✗Assuming stacked
defer f.Close()is harmless because the OS reclaims descriptors at exit - ✗Overlooking that
defer mu.Unlock()in a loop self-deadlocks on the second iteration
Follow-up questions
- →How does wrapping the loop body in a closure or helper function scope the
deferper iteration? - →Why does
defer mu.Unlock()in a loop deadlock whiledefer f.Close()only leaks?
JuniorCodeOccasionalWhat does this counter() closure print across three calls, and why does the count persist?
What does this counter() closure print across three calls, and why does the count persist?
It prints 1 2 3. The returned function closes over the variable count by reference, not by value, so the single count lives on after counter returns and each call increments the same variable. A second counter() makes an independent count starting again at 1.
Common mistakes
- ✗Thinking the closure captures
countby value, resetting it each call - ✗Believing
countis freed whencounterreturns — escape analysis moves it to the heap - ✗Expecting two counters from
counter()to share the samecount
Follow-up questions
- →Why does escape analysis move
countto the heap here? - →How would the output change if
counterreturned two closures sharing onecount?
JuniorDebuggingOccasionalWhy does reassigning a pointer parameter not change the caller's pointer?
Why does reassigning a pointer parameter not change the caller's pointer?
It prints Bob twice. changeName receives a copy of the pointer; reassigning person = &Person{...} only repoints that local copy, leaving the caller untouched. Go has no pass-by-reference. Fix: mutate through it — person.Name = "Alice".
Common mistakes
- ✗Believing a pointer parameter means the caller's pointer itself can be repointed inside the function
- ✗Thinking Go passes arguments by reference rather than always by value (including the pointer value)
- ✗Assuming the reassignment line fails to compile rather than just being a no-op for the caller
Follow-up questions
- →How would you let a function repoint the caller's variable to a brand-new
Person? - →What does
*person = Person{Name: "Alice"}do differently fromperson = &Person{...}?
MiddleCodeOccasionalWhat does this snippet print, and why does wrapping the call in a closure change it?
What does this snippet print, and why does wrapping the call in a closure change it?
It prints 0. defer evaluates its arguments at the defer statement, capturing a's value (0) then; the later a = 20 does not affect it. Wrap it as defer func(){ fmt.Println(a) }() and the closure reads a at return, printing 20.
Common mistakes
- ✗Assuming
deferevaluates its arguments at function return rather than at registration - ✗Believing the closure form captures
aby value at thedeferstatement instead of reading it at return - ✗Confusing argument evaluation timing with the LIFO execution order of deferred calls
Follow-up questions
- →If you
defera method call on a pointer, when is the receiver evaluated? - →How does a
deferinside aforloop interact with the loop variable?
MiddleCodeOccasionalWhat does this snippet with two parameterized deferred closures print, and in what order?
What does this snippet with two parameterized deferred closures print, and in what order?
It prints exiting: 30, second: 20, first: 10. A deferred call's arguments are evaluated at the defer statement, so (a) snapshots a's value — 10, then 20 — and the later a = 30 cannot change them. Deferred calls run in LIFO order.
Common mistakes
- ✗Assuming a deferred closure's parameter is evaluated when the call runs rather than at the
deferstatement - ✗Expecting the deferred calls in registration order, so printing
firstbeforesecond - ✗Thinking the final
a = 30overrides the values already snapshotted into the deferred calls
Follow-up questions
- →How would the output change if the closures read
adirectly instead of taking avalparameter? - →Why does
exiting: 30print before any of the deferred lines?
MiddleTheoryOccasionalWhen can the Go compiler inline a function, and what does inlining buy you?
When can the Go compiler inline a function, and what does inlining buy you?
Inlining copies a callee's body into the caller, removing the call overhead and exposing the code to further optimization (constant folding, escape analysis, dead-code elimination). The compiler inlines only small functions whose cost fits its inlining budget; very large bodies are skipped. Inspect its decisions with go build -gcflags=-m.
Common mistakes
- ✗Thinking the compiler inlines every small function unconditionally
- ✗Believing inlining is about binary size rather than removing call overhead
- ✗Assuming a function with many call sites is never inlined
Follow-up questions
- →Why does a very large function body usually prevent the compiler from inlining it?
- →How can inlining a small accessor enable the compiler to keep a value on the stack?
SeniorCodeRareWhat does this function return, and how does a deferred closure reach the named return value?
What does this function return, and how does a deferred closure reach the named return value?
It returns 2. With a named return value, return 1 assigns 1 to result, then the deferred closure runs and increments result to 2 before the function actually returns. A deferred closure can read and modify named return values after return is reached.
Common mistakes
- ✗Thinking
return 1is final and the deferred closure cannot change the result - ✗Assuming this works with an unnamed return — only a named return value is mutable from
defer - ✗Believing the deferred closure runs before the
returnexpression is assigned, not after
Follow-up questions
- →How is this pattern used to convert a
panicinto a returnederror? - →Would the result change if the return value were unnamed?