skip to content

Writing Push Iterators

Writing a function that returns an iter.Seq means honouring an early stop: when yield returns false you must return immediately, and any file or lock you opened has to be released.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

5

How do you write a Go function returning iter.Seq[string] that a caller can use in a for-range loop?

level: juniorimportance: must knowfreq 40%

answer

  1. the sequence is itself a function
  2. one parameter, and it is yield
  3. outer func only builds the closure
  4. guard every yield with if not

basics

~20 s

Return a closure whose type is func(yield func(string) bool). Inside it, call yield once per element and stop the moment yield returns false. Ranging over the returned function makes the compiler supply yield from the loop body.

solid answer

~40 s

`iter.Seq[V]` is declared as `type Seq[V any] func(yield func(V) bool)`, so a sequence is a function, not a container. You write an outer function that takes whatever arguments you need and returns a closure of that shape; the closure walks the underlying data and calls `yield(v)` for each element. Every call must be guarded: `if !yield(v) { return }`, because a false result means the consuming loop has stopped. Nothing happens when the outer function is called — it only builds the closure — so all the real work, including opening any resource the loop needs, belongs inside the returned closure. `iter.Seq2[K, V]` is the same idea with a two-argument yield, which is how you pair a value with an index, a key or an error.

code

go · 13 lines
go
func Names(dir string) iter.Seq[string] {
	return func(yield func(string) bool) {
		entries, err := os.ReadDir(dir)
		if err != nil {
			return // nowhere to report it: this is why Seq2 exists
		}
		for _, e := range entries {
			if !yield(e.Name()) {
				return
			}
		}
	}
}

go deeper

for a junior

Be ready to write the shape from memory: a function that returns func(yield func(V) bool), calling yield once per element. Know that iter.Seq[V] is that function type, not a collection.

for a middle

Explain that nothing runs until the range loop starts and that the outer function only builds the closure. Be able to say why the closure must take exactly one parameter.

for a senior

Show judgment about when a sequence beats a slice: large or streamed results where callers stop early. Point out that the closure's frame is where any resource the loop needs should be opened.

for a principal

Own the consequence of exporting one: importing teams' loops bind to the shape, and your module's minimum Go version rises. Be ready to say when a plain slice is the better public contract.

## What `iter.Seq` actually is Go's `iter` package declares two types and nothing complicated: ``` type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) ``` So a "sequence" in Go is a **function**, not a data structure. When someone calls it, it pushes values one at a time into the callback it was handed. That callback is conventionally named `yield`. These are called *push* iterators because the iterator is in charge: it runs a loop of its own and calls you for each element. ## The range loop is sugar for calling your function Since Go 1.23 the compiler accepts `for v := range f` when `f` has type `func(func(V) bool)`. It compiles the loop body into a function and passes that function as `yield`. The generated `yield` returns `true` to mean "keep going" and `false` once the loop is finished with you — because the body executed a `break`, a `return`, or otherwise left the loop. That is the whole protocol. ## The shape you write The idiomatic form is a small factory returning a closure: ``` func Names(dir string) iter.Seq[string] { return func(yield func(string) bool) { ... } } ``` Why a factory? Because `iter.Seq[V]` has exactly one parameter — the yield callback — and the range statement supplies it. Any other input the iterator needs (a directory path, a filter, a limit) has to be captured by the closure rather than passed as a parameter. `Names(dir)` therefore returns a value you can hand straight to `range`, or store, or pass to another function. ## The rules for the body 1. **Call `yield` once per element.** The element is the value the caller's loop variable will be bound to. 2. **Check the result every time.** `if !yield(v) { return }`. A false result means the consumer is done and you must produce nothing more. 3. **Do the work lazily.** The factory should not read anything. Callers expect that building the sequence is free and that cost is paid inside the loop. 4. **Own your resources inside the closure.** If the sequence needs a file or a handle open for the duration of the loop, open it inside the closure and `defer` its release there; the closure's frame lives exactly as long as the loop does. 5. **Return normally when the source is exhausted.** There is no sentinel value and no "done" flag: the loop ends when your function returns. ## A first example Listing the names in a directory: ``` func Names(dir string) iter.Seq[string] { return func(yield func(string) bool) { entries, err := os.ReadDir(dir) if err != nil { return } for _, e := range entries { if !yield(e.Name()) { return } } } } ``` The caller writes `for name := range Names("/etc")`. Note the awkward part: the error from `os.ReadDir` has nowhere to go, because the factory already returned before any reading happened. That is exactly what `iter.Seq2[string, error]` is for — a two-value yield where the second value carries the failure. ## `Seq2` in one paragraph `iter.Seq2[K, V]` yields pairs, and the caller writes `for k, v := range s`. The pair is whatever you decide it is: index and value, key and value, or value and error. The contract is identical — call `yield(k, v)`, check the bool, return when it is false. ## When a sequence is the wrong answer A sequence is not automatically better than a slice. If the result is small, already in memory, and callers want to sort it, index it, or keep it, return `[]T` — it is simpler, it can be re-used freely, and it does not require callers to be on a recent Go toolchain. A sequence earns its place when the result is large or streamed, when callers routinely stop early, or when producing a value costs something you would rather not pay for elements nobody looks at. ## The mental model to keep A push iterator is a loop you wrote, temporarily lending its body to somebody else. Everything that follows from that — checking yield's answer, cleaning up when they walk away, deciding where errors go — is a consequence of the fact that the loop is yours and the body is theirs.

  • Why does the code return a closure instead of just declaring a function that takes yield as a second parameter?
    Because `iter.Seq[V]` is defined as a function of exactly one parameter, and the range statement supplies that parameter. A `func(dir string, yield func(string) bool)` is a perfectly good callback API, but it cannot be ranged over. Returning a closure lets you bind `dir` while keeping the single-parameter shape the loop needs.
  • When does the code inside the closure first run?
    When a range loop over the returned value starts, not when the factory is called. The factory only allocates the closure, so building a sequence is cheap and side-effect free; every read, open or network call happens during the loop. That laziness is what callers assume, so do not sneak eager work into the factory.
  • How would you give the caller a position alongside each name?
    Return `iter.Seq2[int, string]` — a closure of type `func(yield func(int, string) bool)` — and call `yield(i, name)` with your own counter. The caller then writes `for i, name := range s`. The same pattern with `iter.Seq2[string, error]` is how a sequence reports failures.

Handing someone a slice is mailing them the whole photo album. Handing them an iter.Seq is standing at the projector: you show one slide at a time, and you stop the moment they say they have seen enough.

saying these in an interview costs you the question

  • Thinks iter.Seq is a container or a channel rather than a function type
  • Believes yield is a keyword the consumer writes in the loop body
  • Does the reading in the factory instead of the returned closure
  • Calls yield for every element and never checks its bool result
  • Claims you must build a slice first and range over that
open as a page

In an iter.Seq iterator you wrote, what must happen when the yield callback returns false, and what if it keeps going?

level: middleimportance: must knowfreq 55%

basics

~20 s

A false result from yield means the consuming loop has stopped. The iterator must produce no further values and return promptly, running its deferred cleanup. Calling yield again after it returned false is a run-time panic.

open as a page

In an iter.Seq2[string, error] directory walker, how do you report a read failure to the ranging caller?

level: middleimportance: should knowfreq 38%

basics

~20 s

Call yield with a zero value for the first half and the non-nil error for the second, then either return or continue past the failed entry. The caller checks the error before using the value.

open as a page

Your iter.Seq2 file iterator opens a file it reads from; how do you guarantee it closes when the caller breaks early?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Open the file inside the returned closure and defer its Close there, never in the factory. That closure's frame lasts exactly as long as the loop and unwinds on an early break, so Close runs once.

open as a page

When should a widely imported Go package export an iter.Seq instead of returning a slice or taking a callback?

level: principalimportance: nice to knowfreq 26%

basics

~10 s

Export a sequence when results are large or streamed, callers stop early, or the loop must hold a resource open. Return a slice when the result is small and already in memory.

open as a page