skip to content

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%

answer

  1. break is greedy for the nearest thing
  2. the switch swallows it first
  3. give the for statement a name
  4. continue does not have this problem

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.

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 lines
go
Scan:
	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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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