skip to content

What does the stop function from iter.Pull do to the suspended sequence, and why defer it?

level: middleimportance: should knowfreq 32%

answer

  1. the sequence is parked inside yield
  2. stop resumes it with a false answer
  3. the iterator's own defers then run
  4. safe to call twice, and after exhaustion
  5. put it on the line after Pull

basics

~20 s

Calling stop resumes the suspended sequence with its pending yield returning false, so the iterator returns and its deferred cleanup runs. Deferring stop right after iter.Pull covers every exit path, including early returns and panics.

solid answer

~50 s

`iter.Pull` returns `stop` as the way to end a sequence you no longer want. The sequence is suspended inside its call to `yield`; `stop` resumes it there with `yield` returning `false`. A correctly written iterator checks that result and returns at once, which lets its own `defer` statements run, so files, database rows or network readers get closed. After `stop`, further calls to `next` return the zero value and `false`. `stop` is safe to call more than once and safe to call when the sequence has already run to exhaustion, where it does nothing. That idempotence is exactly why the idiom is `defer stop()` on the line after the `iter.Pull` call: you pay nothing for the case where you drained the sequence, and you are covered when you break early, return an error, or panic. Skipping it leaves the sequence suspended forever with its cleanup unrun.

code

go · 12 lines
go
next, stop := iter.Pull(records)
defer stop()

for {
	rec, ok := next()
	if !ok {
		return nil
	}
	if rec.ID == target {
		return &rec // stop runs here, so the iterator's defers run too
	}
}

go deeper

for a junior

Remember the two-line idiom: capture stop from iter.Pull, then defer it immediately. Know that skipping it leaves the sequence hanging rather than being cleaned up for you.

for a middle

Explain the mechanism, not just the rule. Stop resumes the suspended sequence with yield returning false, the sequence returns, and its own deferred cleanup runs at that moment.

for a senior

Discuss the failure surface: which resources an unstopped pull holds open, why the defer must sit above every possible early return, and how you would catch a missing stop in review or in a profile.

for a principal

Own the convention. If your codebase publishes sequences that hold connections or file handles, decide whether pull cursors are allowed at all, or wrapped in a helper that cannot be constructed without its stop.

## Where the sequence is when you call stop `iter.Pull` runs the sequence function on a coroutine. Every time that function calls `yield(v)`, control switches back to the caller of `next`, and the sequence is left **suspended inside the yield call**. It is not finished; it is parked mid-execution, holding whatever its local variables hold — an open file, a `bufio.Reader` over a network connection, a half-built buffer. There are exactly two ways for it to become unsuspended and return: 1. **You drain it.** You keep calling `next` until it returns `false`, which happens because the sequence function ran off its end and returned normally. 2. **You stop it.** `stop` resumes the sequence with the pending `yield` call returning `false`. That second mechanism is the whole design. Returning `false` from `yield` is the standard "the consumer is done, wind up" signal in Go's iterator protocol — it is the same signal a `break` in a `for range` over a function sends. A well-written iterator is expected to check it: ``` func Lines(r io.Reader) iter.Seq[string] { return func(yield func(string) bool) { sc := bufio.NewScanner(r) defer report(sc.Err()) for sc.Scan() { if !yield(sc.Text()) { return // consumer stopped; defers run here } } } } ``` So `stop` does not kill anything, and it does not unwind the sequence from outside. It asks the sequence to finish, and the sequence's own `return` path — including every `defer` it registered — runs as a result. ## What stop guarantees to the caller - After `stop` returns, the sequence function has finished. Subsequent calls to `next` return the zero value and `false`. - `stop` may be called **more than once**; the extra calls do nothing. - `stop` may be called after `next` has already reported exhaustion; there is nothing left to stop, so it also does nothing. Those two allowances are what make the `defer` idiom cheap and unconditional. You never have to reason about whether this particular path drained the sequence or abandoned it. ## Why defer, specifically ``` next, stop := iter.Pull(seq) defer stop() ``` Writing `stop()` at the bottom of the function instead is wrong in the same way that a bare `f.Close()` at the bottom is wrong. Between the `Pull` and the end of the function there are usually several ways out: - a `break` because you found what you were looking for and then a `return`; - an error path — `if err != nil { return err }` — inside the loop; - a `panic` in the code that consumes the values; - an early `return` when one of two merged sources runs dry. Every one of those skips a trailing call and none of them skips a `defer`. Put the `defer` on the line immediately after the `Pull`, before any statement that could return, so there is no window in which the cursor exists unprotected. The cost of forgetting is not a minor tidiness issue. The sequence stays suspended inside `yield` for the life of the process: its cleanup never runs, so whatever it holds open stays open, and the coroutine running it is never reclaimed. Repeat that once per request and you accumulate one stranded coroutine per request. ## The case where stop is not enough `stop` relies on the sequence cooperating. If an iterator ignores the `false` result from `yield` and keeps looping, `stop` cannot force it to return — the iterator protocol is a convention enforced by the iterator author, not by the type system. That is why "check `yield`'s result and return immediately" is the first rule of writing a sequence, and why an iterator you did not write is worth a glance before you pull from it in a hot path. ## Contrast with a plain range With `for v := range seq`, you never call `stop` and never need to: `break` makes the compiler-generated `yield` return `false`, the sequence returns, and its defers run before your loop's next statement. The obligation is unique to `iter.Pull`, because pulling hands you a suspended computation with no scope to tie its lifetime to. `defer stop()` is how you tie it to one.

  • Is it a bug to call stop twice, or after next has already returned false?
    No. `stop` is documented as safe to call repeatedly and safe to call once the sequence is already exhausted; the extra calls do nothing. That idempotence is what makes an unconditional `defer stop()` correct even on the path where you drained the sequence yourself.
  • What can stop not protect you from?
    An uncooperative iterator. `stop` works by making the pending `yield` return `false`; if the sequence ignores that result and keeps yielding, `stop` has no way to force it to return. Checking `yield`'s result and returning at once is the iterator author's obligation, not something the caller can impose.
  • Why does a plain for-range over an iter.Seq need no stop call?
    Because `break`, `return` or a panic in the loop body makes the compiler-generated `yield` return `false`, so the sequence returns and its defers run before control leaves the loop. The sequence's lifetime is bounded by the loop statement. A pull cursor has no such scope, which is why it hands you `stop`.

saying these in an interview costs you the question

  • Thinks stop kills the sequence without running its defers
  • Calls stop only on the error path, not on success
  • Believes calling stop twice panics
  • Puts stop at the bottom of the function instead of deferring it
  • Assumes the runtime stops the sequence when the cursor goes out of scope