Why does `break` inside a switch that sits in a Go for loop not exit the loop, and what does?
answer
- break is greedy for the nearest thing
- the switch swallows it first
- give the for statement a name
- continue does not have this problem
basics
~20 sA 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.
solid answer
~50 s`break` terminates the innermost statement it can break out of. Inside a case body that statement is the `switch`, so the loop cheerfully starts its next iteration — the code reads as if it stops and it does not. The fix is a label on the `for` statement and `break Scan`, which terminates the labeled loop instead. `continue` is different and worth saying explicitly: it only ever applies to a `for`, so a bare `continue` inside a switch inside a loop already starts the next iteration correctly; a labeled `continue Outer` is needed only to skip to the next iteration of an *outer* loop from a nested one. Labels live in their own namespace, are scoped to the function body and excluded from nested function literals, and an unused label is a compile error, so a stale one cannot rot silently.
code
go · 10 linesScan:
for _, tok := range tokens {
switch tok.Kind {
case kindEOF:
break Scan // leaves the for loop; a bare break would leave only the switch
case kindComment:
continue // already starts the next iteration of the for loop
}
emit(tok)
}go deeper
Know that a break written inside a case ends only the switch, and that the loop around it carries on to the next iteration as if nothing happened.
Explain that break binds to the innermost enclosing switch or for, that a label on the for plus break Label leaves the loop, and that continue already targets the loop without a label.
In review, choose between a labeled break and extracting the loop into a function that returns, and be able to say why a boolean flag threaded past the switch is the worst of the three.
Decide how much nesting your codebase tolerates before a labeled jump counts as a smell, and whether the team's standard answer is extraction, a small state machine, or an accepted label.
## What `break` binds to A bare `break` terminates the **innermost enclosing** `for` or `switch` (or `select`) statement — whichever one encloses it most tightly. That rule is short, but its consequence catches nearly everyone once: ``` for _, tok := range tokens { switch tok.Kind { case kindEOF: break // ends the switch, not the loop } emit(tok) } ``` The `break` ends the `switch`. Control lands on `emit(tok)`, the loop runs its next iteration, and the program keeps scanning past the end token. Nothing is reported: the code compiles, and it does something reasonable-looking that is not what the author meant. This is not unique to Go — the same rule holds in C and Java — but Go programs hit it more often because the expressionless switch and the multi-value case make switches inside loops a natural way to write a scanner or dispatcher, and because Go has no `break` needed at the end of each clause, so the one `break` you *do* write looks unusually significant. ## Labels The fix is a labeled statement. A label is an identifier followed by a colon, written immediately before the statement it names, and `gofmt` outdents it one level: ``` Scan: for _, tok := range tokens { switch tok.Kind { case kindEOF: break Scan // terminates the for loop } emit(tok) } ``` `break Scan` terminates the statement the label names. The label of a `break` must name an enclosing `for`, `switch` or `select` — you may label a switch and break out of it from a nested block, though that is rare. The label of a `continue` must name an enclosing **`for`** statement; anything else is a compile error. Three properties of labels are worth knowing: - **Their scope is the function body** in which they are declared, and it **excludes the body of any nested function literal**. You cannot write `break Scan` inside a closure defined in the loop, even lexically inside the labeled loop — the compiler rejects the reference. A callback has to signal its caller instead, by returning a value the loop inspects. - **They live in their own namespace.** A label named `Scan` does not collide with a variable named `Scan`, so labels are conventionally capitalised without any exported meaning. - **An unused label is a compile-time error**, not a warning. If you delete the jump but leave the label, the build fails. That is deliberate: a leftover label usually means control flow was edited and the code no longer does what its shape suggests. ## `continue` does not have this problem `continue` has no relationship with `switch` at all — it applies to the innermost enclosing `for`. So inside a switch inside a loop, a bare `continue` starts the next iteration of that loop, which is usually exactly what you want, and writing `continue Scan` for the immediately enclosing loop is redundant. A labeled `continue` earns its keep only with **nested loops**, where you want the next iteration of the outer one: ``` Outer: for _, file := range files { for _, line := range file.Lines { if line.Broken { continue Outer // next file, not next line } } } ``` ## The alternatives, and when to prefer them A labeled break is not the only way out, and the three options rank differently in review: 1. **Extract the loop into its own function and `return`.** Usually the best answer. The exit condition becomes a returned value or error, the nesting drops a level, and the loop becomes independently testable. If the loop body is doing enough work to need a switch, it is often doing enough to be a function. 2. **Label the loop and `break Label`.** Correct, explicit, and checked by the compiler — the label must name a real enclosing loop, and it cannot be dangling. Right when extraction would mean threading four locals through a parameter list. 3. **Set a boolean flag in the case and test it after the switch.** The worst of the three. It adds state, it delays the exit by the rest of the body, and a later editor who inserts code between the switch and the test introduces a bug that no tool will find. In a review, a labeled break in a two-level loop is fine. A function with three labels is a signal that the control flow wants restructuring rather than more labels. ## Summary `break` binds to the innermost `for` or `switch`; inside a case that is the switch. Label the `for` and use `break Label` to leave it. `continue` already targets the loop, and needs a label only to reach an outer one. Labels are function-scoped, invisible inside closures, and unused ones fail the build.
- Can you write `break Scan` from inside a function literal defined in the loop body?No. A label's scope is the body of the function that declares it and explicitly excludes any nested function literal, so the compiler rejects the reference. A closure has to signal its caller instead — return a value or an error the loop inspects — which is why a callback in a loop body cannot simply jump out of it.
- Can a label be attached to something other than a for loop?For `break`, yes: you may label a `switch` and break out of it from a nested block, though it is rare. `continue` is stricter — its label must name an enclosing `for` statement, and anything else is a compile error. `goto` can name any labeled statement in the same function, subject to its own jump restrictions.
- What does the compiler do with a label you declare but never reference?It refuses to build: an unused label is a compile-time error, not a warning. That is deliberate — a leftover label almost always means a jump was deleted or renamed, and the loop no longer behaves the way its labeled shape implies.
- When would you prefer extracting the loop into a function over a labeled break?Whenever the exit condition is meaningful to the caller. Returning a value or an error names *why* the loop stopped, removes a nesting level, and makes the loop testable on its own. Keep the label when extraction would mean threading several locals through a parameter list just to satisfy the compiler.
saying these in an interview costs you the question
- Says a bare break inside a case exits the enclosing loop
- Thinks a bare continue in a switch moves to the next case
- Uses a boolean flag tested after the switch instead of a label
- Believes a label can be referenced from inside a closure
- Assumes an unused label is silently ignored