skip to content

What does iter.Pull return, and why use it instead of ranging over an iter.Seq?

level: juniorimportance: must knowfreq 40%

answer

  1. two functions come back, not one
  2. one advances, one shuts down
  3. the bool is exhaustion, not error
  4. you crank it; the sequence no longer drives
  5. needed when two sequences must move in step

basics

~20 s

iter.Pull(seq) returns two functions: next, which gives the next value plus a bool that is false once the sequence is exhausted, and stop, which ends it early. It turns a sequence into a cursor you advance yourself.

solid answer

~50 s

`iter.Pull(seq)` takes an `iter.Seq[V]` and returns `next func() (V, bool)` and `stop func()`. Each call to `next` produces the following value with `true`; once the sequence is over it returns the zero value and `false` and keeps doing so. `stop` ends the sequence early and releases what it holds. A `for range` over an `iter.Seq` puts the sequence in charge — your loop body runs inside the producer's stack, and you get values only as fast as it hands them over. That is fine for one sequence, but it cannot interleave two, because you cannot run two `range` loops in lockstep. `iter.Pull` inverts the control flow so you decide when the next value is produced, which is what merging, zipping, or one-token lookahead needs. It costs a coroutine per pull, so range directly when a plain loop will do.

code

go · 10 lines
go
next, stop := iter.Pull(seq)
defer stop()

for {
	v, ok := next()
	if !ok {
		break // the sequence is exhausted
	}
	handle(v)
}

go deeper

for a junior

Be ready to name both returned functions and say what the bool from next means: false is exhaustion, not an error. Know the idiomatic loop that calls next until ok is false.

for a middle

Explain why a for-range over an iter.Seq cannot interleave two sequences: the loop body becomes the yield callback, so the sequence drives and runs to completion. Then explain how Pull inverts that.

for a senior

Show you know the price. A pull costs a coroutine and a control switch per element and creates a cleanup obligation, so justify reaching for it over a plain range in a hot path.

for a principal

Frame it as an API question: a package that exposes iter.Seq lets callers choose either style, whereas one that exposes a hand-rolled cursor type forces the pull cost on everybody.

## Two shapes of iterator Go 1.23 added the `iter` package and the ability to `range` over a function. The type it standardises is the **push** iterator: ``` type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) ``` A `Seq` is a function that, when called, walks whatever it walks and calls `yield` once per element. `for v := range seq` compiles into exactly that: the compiler turns the loop body into the `yield` closure and hands it to the sequence. The consequence is that **the producer drives**. Your loop body runs nested inside the sequence's own call stack, and it runs when the sequence decides to call `yield`. That is the right default: it is cheap, it needs no extra goroutine, and cleanup inside the sequence (a `defer f.Close()`) works normally. But it means you can only be in the middle of *one* such sequence at a time in a given function. You cannot write "look at the head of A, look at the head of B, take the smaller one" with two `range` loops, because each `range` runs its sequence to completion before the next statement executes. ## What iter.Pull gives you ``` func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func()) func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func()) ``` `Pull` converts a push sequence into a **pull** iterator: a cursor you crank. - **`next`** — each call resumes the sequence just far enough to produce one more value. It returns `(value, true)` while the sequence still has elements, and `(zero value, false)` once the sequence's function has returned. After that point it keeps returning the zero value and `false`; it does not restart and it does not panic. - **`stop`** — ends the sequence early, for when you stop caring before it is exhausted. It is valid to call `stop` more than once, and valid to call it after `next` has already reported exhaustion, where it simply does nothing. Two things about the `bool` trip people up. It is **not an error flag** — errors in a Go iterator travel as a value, typically through `iter.Seq2[T, error]`. And it does **not** mean "there are more values after this one": it describes the value returned by *this* call. The idiomatic loop is therefore: ``` for { v, ok := next() if !ok { break } use(v) } ``` ## How it works underneath `Pull` runs the sequence function on a coroutine. When the sequence calls `yield`, control switches back to whoever called `next`; when you call `next` again, control switches back into the sequence at the point it yielded. Only one side runs at any moment — this is control transfer, not parallelism — so values crossing between them need no locking. The flip side is that the returned `next` and `stop` are **not safe for concurrent use**: one goroutine must own the cursor. The sequence body does not start running until the first call to `next`. So `Pull` itself does no work beyond setting up the cursor. ## What it costs, and the obligation it creates A pull is more expensive than a plain `range`: there is a coroutine and a control switch per element, versus a direct function call. Reach for it when you actually need the cursor — merging two sorted streams, zipping, a parser that needs one token of lookahead, or feeding an API that asks "give me the next one" — and range directly otherwise. It also creates an obligation the push form does not have. With `range`, the sequence always gets to return, so its deferred cleanup always runs. With `Pull`, if you walk away from the cursor before it is exhausted and never call `stop`, the sequence is left suspended mid-`yield`: its cleanup never runs and the coroutine is never reclaimed. That is why `defer stop()` goes on the line immediately after every `iter.Pull` call. ## Pull2 `iter.Pull2` is the same mechanism for `iter.Seq2[K, V]`; its `next` returns three results, `(K, V, bool)`, with the same meaning for the final `bool`. Everything about `stop`, exhaustion and cleanup is identical.

  • What does iter.Pull2 return, and how does it differ?
    `iter.Pull2` takes an `iter.Seq2[K, V]` and returns `next func() (K, V, bool)` plus the same `stop func()`. The only difference is that `next` hands back two values instead of one; the trailing bool still means "a pair was produced", and the stop obligation is identical.
  • When does the sequence function actually begin running?
    On the first call to `next`, not at the `iter.Pull` call itself. `Pull` only sets up the cursor. So a sequence whose first act is opening a file has not opened anything until you ask for the first value.
  • Can two goroutines share one pull cursor?
    No. The `next` and `stop` functions returned by `iter.Pull` must not be called from multiple goroutines simultaneously. Underneath, the pull is a control switch between the caller and the suspended sequence, and it assumes a single driver. If several goroutines need values, have one goroutine own the cursor and hand values out.

Ranging over an iter.Seq is a conveyor belt that keeps handing you items at its own pace. iter.Pull replaces it with a crank you turn once per item, plus a switch that shuts the machine down.

saying these in an interview costs you the question

  • Thinks iter.Pull returns a slice of all the values
  • Says next's bool reports an error from the sequence
  • Reads the bool as 'more values remain after this one'
  • Claims two range loops can walk two sequences in step
  • Believes iter.Pull runs the sequence in parallel with the caller