skip to content

Functions, Closures and Control Flow

Functions as values, the multiple-return convention that shapes every Go API, defer, and the deliberately small set of control-flow statements. These are the mechanics you use in every file, and the loop-variable and defer questions are perennial interview material.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

25

In Go, does a function literal capture an outer variable's value at creation, or the variable itself?

level: juniorimportance: must knowfreq 72%

answer

  1. not a photograph, a live wire
  2. reassign after creating, then call it
  3. writes inside are visible outside
  4. the variable is shared, not copied
  5. snapshot means a brand-new variable

basics

~20 s

A Go function literal captures the variable itself, not a snapshot of its value. Assignments made after the literal is created are visible when it runs, and assignments made inside it are visible to the enclosing code.

solid answer

~50 s

Go closures capture variables by reference, not by value. When a function literal mentions a local from the enclosing function, the two share one variable: if the outer code assigns to it after the literal is created, calling the literal reads the new value, and if the literal assigns to it, the enclosing function sees that write. So `msg := "before"; f := func() { fmt.Println(msg) }; msg = "after"` prints `after`. There is no effectively-final restriction as in some languages, and no capture-by-value mode. If you want a snapshot of the value at creation time, you must make a new variable — pass it as a parameter to the literal, or declare a fresh one (`m := msg`) and capture that instead. The captured variable stays valid for as long as the closure does, even after the enclosing function has returned.

code

go · 4 lines
go
msg := "before"
f := func() { fmt.Println(msg) }
msg = "after"
f() // prints: after

go deeper

for a junior

Be ready to state the rule in one sentence and demonstrate it: assign to the variable after creating the literal, call the literal, and say what prints. Know that there is no value-capture form in Go.

for a middle

Explain that the variable is shared in both directions, that only referenced variables are captured, and show the two ways to force a snapshot: a parameter, or a freshly declared local.

for a senior

Show where the rule causes production bugs — a handler reading config reassigned during setup, a shared counter incremented from several goroutines — and describe the review habit that catches capture of mutable state.

for a principal

Frame it as an API rule: decide whether your callbacks take their inputs as parameters or capture them, since captured state is invisible at the call site and turns into shared mutable state the moment a caller runs the callback concurrently.

## What a function literal is A function literal in Go is an anonymous function written inline: `func(a int) int { return a + n }`. It is an ordinary value of a function type, so it can be assigned to a variable, stored in a struct field or a slice, passed to another function, and returned. What makes it a *closure* is that its body may mention identifiers declared in the function that surrounds it. ## The rule: the variable, not the value The Go specification says function literals are closures: they may refer to variables defined in a surrounding function, and *those variables are shared between the surrounding function and the function literal*. That word — shared — is the whole answer. The literal does not copy the value it saw when the literal was evaluated. It refers to the same storage the enclosing code refers to. Two symmetric consequences follow. **Later writes from outside are visible inside.** ```go msg := "before" f := func() { fmt.Println(msg) } msg = "after" f() // prints: after ``` The literal was created while `msg` held `before`, but it never stored `before` anywhere. It reads `msg` when it runs. **Writes from inside are visible outside.** ```go total := 0 add := func(n int) { total += n } add(2) add(3) // total is now 5 ``` The closure mutates the caller's variable directly. This is legal Go and is the basis of counters, accumulators and callback-driven state. ## How this differs from other languages Engineers arriving from Java or C# often expect a rule that a captured local must be final or effectively final, and that only its value travels into the lambda. Go has no such rule: a captured variable is fully mutable from both sides. Engineers arriving from C expect the opposite hazard — a pointer to a local that dies when the frame returns. Go has no such hazard either: if a closure still refers to a variable, that variable remains valid for as long as the closure is reachable, so returning a closure over a local is safe and idiomatic. (Where the compiler chooses to place such a variable is a separate topic; from the language's point of view all you need is that the variable outlives the frame.) ## Getting a snapshot when you want one Because there is no capture-by-value form, a value snapshot has to be made explicitly, and both idioms create a *new variable* for the closure to capture: 1. **Pass it as a parameter.** `func(v string) func() { return func() { fmt.Println(v) } }(msg)` — the parameter `v` is a fresh variable initialised from `msg` at call time; later writes to `msg` cannot reach it. 2. **Declare a fresh local.** `m := msg` immediately before the literal, then capture `m`. The common shadowing idiom `v := v` is exactly this trick, reusing the name. A third option is to stop capturing at all: give the closure everything it needs through its parameters, so it becomes a pure function value. ## Which variables are captured Only the variables the literal's body actually references. A closure does not drag in the whole surrounding frame, and it does not capture names it never mentions. Nested literals capture through: an inner literal referring to a variable of the outermost function keeps that one variable shared across all three scopes. Capture is per-variable, not per-name-occurrence. Two literals created in the same scope that both mention `n` share the same `n` with each other and with the enclosing code. ## Where this bites in practice * Building a list of callbacks in a loop and expecting each one to remember the iteration it was built in — that depends on whether the loop gives each iteration its own variable. * Registering a handler that reads a configuration local, then reassigning that local during setup, and being surprised the handler uses the later value. * Assuming a closure that increments a shared counter is safe to run from several goroutines; the counter is one variable, so concurrent increments need a mutex or an atomic type. * Reading a variable in a deferred literal and expecting the value from the moment the literal was written rather than from the moment it runs. ## The mental model to keep A closure is not a photograph of the surrounding variables; it is a set of live wires to them. Everything else — the loop-variable trap, closures as counters, callbacks that keep data alive longer than expected — falls out of that single fact.

  • How do you give a Go function literal a snapshot of a variable's value at creation time?
    Create a new variable for it to capture. Either pass the value as a parameter to the literal and let the closure use the parameter, or declare a fresh local (`m := msg`) immediately before the literal and capture that. Both work because the new variable is never reassigned afterwards; there is no capture-by-value syntax in Go.
  • If a Go closure outlives the function that declared a captured local, is that variable still valid?
    Yes. Go guarantees the captured variable stays valid as long as the closure that refers to it is reachable, so returning a closure over a local is safe and common. It is still the same single variable, not a copy, so any other closure over it sees the same writes.
  • Does a Go function literal capture every local of the enclosing function?
    No — only the variables its body actually references. Locals the literal never mentions are not captured and are not kept alive by it. This matters when a closure is stored long-term: reference one small field instead of a large struct and only the small one is retained.

A closure holds the key to a mailbox, not a copy of the letter that was inside when it got the key. Whoever opens the box later sees whatever is in there now.

saying these in an interview costs you the question

  • Says the closure copies the value at creation time
  • Claims captured variables must be final or unmodified
  • Thinks assignments inside the literal are invisible outside
  • Fears a dangling reference after the enclosing function returns
  • Believes the closure captures the whole enclosing frame
open as a page

In Go, what are the three non-range forms of for, and how do you write a while loop?

level: juniorimportance: must knowfreq 78%

basics

~20 s

Go's for has three non-range forms: three-clause (init; condition; post), condition-only, and infinite (for with no clauses at all). The condition-only form is Go's while loop - the language has no while and no do-while keyword.

open as a page

What does Go's `defer` statement do, and when does the deferred call actually run?

level: juniorimportance: must knowfreq 85%

basics

~10 s

defer postpones a function call until the surrounding function finishes, not until the enclosing block ends. Several deferred calls run in reverse registration order, last registered first, and they run on every return path.

open as a page

In Go, how must a caller handle both results of a function returning `(int, bool)`?

level: juniorimportance: must knowfreq 78%

basics

~10 s

A Go call returning two values needs two targets: port, ok := parsePort(s). Write the blank identifier _ for any result you do not want. Binding one variable to it is a compile error.

open as a page

In a Go switch statement, what happens at the end of a case body, and what does `fallthrough` do?

level: juniorimportance: must knowfreq 70%

basics

~20 s

In Go a case body ends the switch automatically — there is no implicit fall-through and no break is needed. The fallthrough keyword, written as the last statement of a case, forces control into the next case's body.

open as a page

In Go, what does `func sum(xs ...int)` receive as `xs`, and how do you pass an existing `[]int`?

level: juniorimportance: must knowfreq 78%

basics

~10 s

Inside the function, xs is an ordinary []int holding the arguments. Writing sum(1, 2, 3) makes Go build that slice for you; to pass a slice you already hold, spread it with sum(nums...).

open as a page

Why did closures created in a Go for loop all observe the last iteration's value before Go 1.22?

level: middleimportance: must knowfreq 78%

basics

~20 s

Before Go 1.22 a for loop declared its loop variables once and reassigned them each iteration, so every closure captured that one shared variable and read its final value. Since Go 1.22 each iteration declares its own copy.

open as a page

In Go, why does ranging over a slice of structs with for _, r := range rows { r.count++ } leave rows unchanged?

level: middleimportance: must knowfreq 66%

basics

~20 s

range assigns a copy of each element into r, so r.count++ increments a copy that is thrown away at the end of the iteration. To modify the slice, index into it: for i := range rows { rows[i].count++ }.

open as a page

In Go, what does range yield for a slice, a map, a string, and an integer?

level: middleimportance: must knowfreq 74%

basics

~20 s

A slice yields the index and a copy of the element; a map yields a key and its value in unspecified order; a string yields the byte offset and the decoded rune; an integer n yields the values 0 through n-1.

open as a page

When are the arguments to a Go deferred call evaluated — at the `defer` line or at return?

level: middleimportance: must knowfreq 70%

basics

~20 s

At the defer line. Go evaluates the deferred call's function value, receiver and arguments immediately and stores those values; only the invocation waits until the function returns. Wrap the call in a closure when you need values read later.

open as a page

Why does `break` inside a switch that sits in a Go for loop not exit the loop, and what does?

level: middleimportance: must knowfreq 55%

basics

~20 s

A break terminates the innermost enclosing switch or for, and inside a case body that is the switch, so the loop keeps going. Label the for statement and break with that label to leave the loop.

open as a page

Why do two closures returned by the same Go function share state, while separate calls do not?

level: middleimportance: should knowfreq 52%

basics

~20 s

Each call to the enclosing function creates its own locals, so closures returned by different calls see different variables. Closures returned by one call capture the same variables, so a write through either is visible to both.

open as a page

How can a deferred closure change the value a Go function has already returned?

level: middleimportance: should knowfreq 52%

basics

~20 s

Only when the function's results are named. A return statement first assigns the result variables, then runs the deferred calls, then hands control back — so a deferred closure that assigns to a named result changes what the caller receives.

open as a page

When should a Go function return `(T, bool)` in comma-ok style instead of `(T, error)`?

level: middleimportance: should knowfreq 52%

basics

~10 s

Return a bool when absence is ordinary and has one uninteresting cause, as with os.LookupEnv. Return an error when there are several distinguishable causes, or the caller must report why it failed.

open as a page

What are named result parameters in a Go function signature, and what do they hold on entry?

level: middleimportance: should knowfreq 62%

basics

~20 s

Naming results, as in func copyInto(dst, src []byte) (n int, truncated bool), declares them as local variables already set to their zero values. Explicit returns still work; a bare return ships whatever they currently hold.

open as a page

In Go, what does a `switch` with no expression after the keyword compare its cases against?

level: middleimportance: should knowfreq 55%

basics

~10 s

It compares against the boolean value true, so every case is a boolean expression and the first one that is true runs. This expressionless form is Go's idiomatic replacement for a long if/else-if chain.

open as a page

In Go, when you forward a slice with `f(s...)`, can the callee's writes to that parameter change the caller's slice?

level: middleimportance: should knowfreq 52%

basics

~20 s

Yes. The three-dot spread passes the caller's slice itself rather than a copy, so both sides index one backing array. Any element the callee writes through the variadic parameter is visible to the caller afterwards.

open as a page

Why can't a Go `[]string` be spread into a `...any` parameter, and what does the workaround cost?

level: middleimportance: should knowfreq 44%

basics

~20 s

The spread requires a slice already assignable to []any, and []string is not: Go's slice types are invariant. You must build a []any and convert element by element, costing one allocation plus one conversion per element.

open as a page

If you append to a Go slice while ranging over it, does the loop visit the newly appended elements?

level: seniorimportance: should knowfreq 44%

basics

~20 s

No. range evaluates the slice expression once and fixes the iteration count from its length before the first pass, so the loop runs exactly the original len(s) times. Appended elements are in the slice afterwards but are never visited.

open as a page

A Go backup archiver that calls `defer f.Close()` inside its walk loop dies with "too many open files". Why, and how do you fix it?

level: seniorimportance: should knowfreq 55%

basics

~20 s

defer is scoped to the enclosing function, not the loop body, so every file opened during the walk stays open until that function returns. Give each file its own function call, so its deferred Close runs per file.

open as a page

Go's compiler never checks that a switch covers every constant of a type — how do you stop an unhandled case from failing silently?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Go has no exhaustiveness check, so make the gap loud instead of silent: end the switch with a default that panics or returns an error rather than doing nothing, and add a test that walks every declared constant through it.

open as a page

What restrictions does Go place on `goto`, and when is it still the right tool?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

A goto may only target a label in the same function, may not jump into a block, and may not skip a variable declaration in scope at the label. It survives for shared cleanup and generated code.

open as a page

In Go, how can a stored callback closure keep a large captured []byte alive for the process's lifetime?

level: seniorimportance: nice to knowfreq 35%

basics

~20 s

A closure keeps every variable it references reachable for as long as the closure value itself is reachable. Registering a callback that mentions a large slice in a long-lived table therefore retains that slice, and everything its backing array holds, indefinitely.

open as a page

Why do naked returns in a long Go function with named results make bugs easy to miss?

level: seniorimportance: nice to knowfreq 36%

basics

~10 s

A bare return ships whatever the named results hold at that line, and names nothing itself. In a long function a branch that skips an assignment silently returns a plausible zero value nobody chose.

open as a page

When does a call to a Go `...any` variadic helper allocate, and how would you keep that off a hot path?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Each call site listing loose arguments builds a fresh slice for the variadic parameter, which lands on the heap only if it escapes. Forwarding with args... builds nothing extra; a fixed-arity variant avoids the slice altogether.

open as a page