skip to content

In a `for range` over an `iter.Seq`, what does `break` in the loop body do to the iterator function?

level: middleimportance: should knowfreq 55%

answer

  1. the loop body is now a function
  2. it cannot jump out of the producer
  3. the exit has to be a return value
  4. false means: no more, run your defers

basics

~20 s

The yield call that delivered the current element returns false instead of true. That is the iterator's signal to stop: it should run its cleanup and return without calling yield again. Calling yield after it has returned false panics.

solid answer

~50 s

The loop body is compiled into the `yield` callback, so `break` cannot unwind anything directly — it has to be reported as a return value. The `yield(v)` call that delivered the current element returns `false`, and the iterator is expected to stop: no more `yield` calls, run any deferred cleanup, return. Only then does control resume after the loop. The same applies to `return` from the enclosing function, `goto` out of the loop, and `break` or `continue` targeting an enclosing label; a plain `continue` is not an exit, so `yield` returns `true` and production carries on. The compiler guards the generated `yield`, so a producer that ignores a `false` and keeps yielding panics at that call rather than silently re-running the loop body. A panic in the body travels out through `yield` into the iterator, which unwinds and runs its defers.

code

go · 6 lines
go
func first(seq iter.Seq[int]) (int, bool) {
	for v := range seq {
		return v, true
	}
	return 0, false
}

go deeper

for a junior

Remember the one-liner: break makes the yield call return false, and the iterator is supposed to stop. Know that you are allowed to break out of such a loop like any other.

for a middle

Explain why a return value is needed at all — the body is a callback inside the producer's frame, so it cannot jump out — and list what counts as an exit versus a plain continue.

for a senior

Demonstrate the ordering: results are set, yield returns false, the iterator returns and runs its defers, then the enclosing function returns. Mention the runtime guard that panics on a yield after false.

for a principal

Frame it as an API guarantee you can build on: because early exit is a first-class signal, a library can hold a connection or a lock across a traversal and still be sure of releasing it. Decide what your packages promise to release on an abandoned range.

## Why a return value carries the break When you range over an `iter.Seq[V]` — that is, a `func(yield func(V) bool)` — the compiler moves your loop body into the `yield` function and hands it to the sequence. That inversion creates a problem: the body is now a separate function called from inside the producer's own loop, so a `break` in it cannot simply jump out. There is a whole stack frame of somebody else's code in the way, possibly with a file open, a lock held, or a `defer` registered. Go solves it with the `bool` result. `yield` returns `true` for "I took that one, keep going" and `false` for "the loop is over, stop". Every early exit from the loop body is compiled into `return false` from `yield`. ## What counts as an exit These all make `yield` return `false`: - `break` - `return` from the function that contains the loop - `goto` to a label outside the loop - `break` or `continue` targeting a label on an enclosing loop - a `panic` — technically not a `false` return but an unwind through `yield`, discussed below A plain `continue` is **not** an exit. It just ends this iteration of the body, so the generated `yield` returns `true` normally and the iterator produces the next element. ## The ordering that surprises people Consider taking the first element: ```go func first(seq iter.Seq[int]) (int, bool) { for v := range seq { return v, true } return 0, false } ``` The `return v, true` does not exit `first` immediately. The result values are computed and stored, then `yield` returns `false`, then the iterator function gets to finish — running its deferred cleanup and returning — and only after that does `first` actually return. This ordering is the whole point of the design: a producer holding a resource always gets a chance to release it, even when the consumer bails out on the first element. ## What the iterator must do with `false` From the producing side the contract is: check `yield`'s result and stop at once. The usual shape is `if !yield(v) { return }` inside the production loop. "Stop" means stop calling `yield` — it does not mean skip cleanup. Deferred calls inside the iterator run as it returns, exactly as in any other function. ## The guard, and the panic it raises The generated `yield` is stateful and checked. If the iterator ignores the `false` and calls `yield` again, the runtime panics rather than running your loop body after the loop has exited. Likewise, calling `yield` after the sequence function itself has returned panics. These guards exist because the alternative — a loop body silently executing after its `break`, mutating variables the surrounding function has already moved past — would be an unfindable class of bug. A loud panic at the offending call site is far cheaper. ## Panic in the loop body If your loop body panics, the panic propagates out of `yield` into the iterator function, which unwinds like any other frame: its deferred functions run, and the panic continues on to the caller of the range statement. So a producer that wrote `defer f.Close()` still closes the file. The one thing that will *not* happen is the iterator continuing to yield. ## Consumer-side implications Two practical consequences for the code you write: First, an early `break` is cheap and safe. You do not have to drain a sequence you no longer need, and you should not write a `for` loop that keeps ranging "to be polite". The `false` result is the polite exit. Second, cleanup you rely on happens inside the producer, not in your loop. If you need something released when you stop early, it must be the sequence function's `defer`, not yours — and if the sequence's documentation does not say what it releases, that is a question for its author. ## What to say in an interview "The loop body becomes the `yield` function, so `break` is reported as `yield` returning `false`. Same for `return`, `goto`, and labelled `break`/`continue`; a plain `continue` returns `true`. The iterator is expected to stop yielding and return, running its defers, before control resumes after the loop — and if it ignores the `false` and yields again, the runtime panics."

  • What does a plain `continue` in the loop body make `yield` return?
    `true`. `continue` ends only that one iteration of the body, so the generated `yield` returns normally and the iterator keeps producing. Only an exit from the loop — `break`, `return`, `goto`, or a `break`/`continue` aimed at an enclosing label — is reported as `false`.
  • What happens if an iterator ignores a `false` result and calls `yield` again?
    The runtime panics. The compiler-generated `yield` tracks whether the loop has finished, so a badly written producer fails loudly at the offending call instead of silently re-running the loop body after the loop exited. The same guard catches a `yield` called after the sequence function has already returned.
  • If the loop body panics, does cleanup deferred inside the iterator still run?
    Yes. The panic propagates out of the `yield` call into the iterator function, which unwinds like any other frame — its deferred calls execute — and the panic then continues to the caller of the range statement. What stops is production, not cleanup.

saying these in an interview costs you the question

  • Thinks break aborts the iterator mid-call, skipping its cleanup
  • Says a loop over an iterator cannot exit early
  • Believes yield's bool reports whether the element was valid
  • Assumes continue also makes yield return false
  • Claims the iterator keeps running in the background after break