skip to content

iter.Seq and iter.Seq2

An iterator is just a func(yield func(V) bool), and yield returning false means the consumer broke out. Reading that signature aloud is a standard senior-interview check.

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

questions

4

What is Go's `iter.Seq[V]` type, and what does a value of that type actually hold?

level: juniorimportance: must knowfreq 62%

answer

  1. an iterator here is not a struct
  2. the iter package declares two named types
  3. it takes a callback and calls it
  4. yield func(V) bool, once per element

basics

~20 s

iter.Seq[V] is a function type: func(yield func(V) bool). An iterator is just a function that calls yield once per element, and each yield call becomes one iteration of the for range loop that consumes it.

solid answer

~50 s

The standard library's `iter` package declares `type Seq[V any] func(yield func(V) bool)`. So a value of that type is an ordinary function value — not a struct, not an interface, and it carries no cursor or position of its own. Its job is to produce elements by calling the `yield` callback it is handed, once per element, and to return when there are no more. When you write `for v := range seq`, the compiler packages the loop body into that `yield` function and calls `seq` with it, so every `yield(v)` runs one iteration of your loop. Its sibling is `type Seq2[K, V any] func(yield func(K, V) bool)`, which delivers two values per call. Because a `Seq` is only a function, you can store it in a struct field, pass it to another package, or call it yourself with your own callback.

code

go · 3 lines
go
type Seq[V any] func(yield func(V) bool)

type Seq2[K, V any] func(yield func(K, V) bool)

go deeper

for a junior

Be ready to write the declaration from memory: iter.Seq[V] is func(yield func(V) bool). Say out loud that an iterator here is a function, and that each call to yield is one turn of the loop.

for a middle

Explain the mechanics: the compiler packages the loop body into the yield callback and calls the sequence function with it, so control sits inside the producer for the whole traversal. Note that Seq2 delivers two values per yield call.

for a senior

Show that you know the type guarantees nothing beyond the signature — not finiteness, not repeatability, not concurrency safety — and that those properties belong in the producing package's documentation.

for a principal

Own the vocabulary decision: agreeing that every package in a codebase returns iter.Seq rather than a bespoke callback type is what lets sequences flow between teams without adapters. Weigh that against the Go version floor it imposes on consumers.

## The declaration Go's `iter` package contains almost nothing: two type declarations and two functions. The two types are ```go type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool) ``` That is the whole definition of an iterator in Go. An `iter.Seq[V]` value is a **function value**. It is not an object with a `Next` method, not an interface, not a channel, and not a lazy collection holding buffered elements. If you come from a language where iteration is an interface with `hasNext`/`next` or a class implementing `IEnumerable`, this is the single adjustment to make: Go turned the relationship inside out. ## Push, not pull In the interface style, the *consumer* drives: it repeatedly asks the iterator for the next element. Go's `Seq` is the opposite — a **push** iterator. The consumer hands the producer a callback, conventionally named `yield`, and the producer runs its own loop, calling `yield` once per element. Control lives inside the iterator function for the whole traversal; the loop body is a guest that gets called back. That is why the iterator does not need to store a position between calls. A pull iterator must remember where it stopped, because it returns to the caller after each element. A push iterator never stops in the middle — its own local variables, its own `for` loop, its own open file handle, all stay live on its stack for the duration. ## What `for range` does with it Given `seq` of type `iter.Seq[string]`, the loop ```go for s := range seq { use(s) } ``` compiles to roughly: build a function `func(s string) bool { use(s); return true }`, call `seq` with it, and let each invocation of that function be one iteration. The `bool` result is how the loop tells the producer whether to keep going — `true` for continue, `false` for stop. A useful consequence: because a `Seq` is a plain function, you are not obliged to use `for range` at all. You can call it directly with a callback of your own: ```go seq(func(s string) bool { fmt.Println(s) return true }) ``` This is the same traversal, written by hand. It is occasionally handy inside a helper that needs to do something the loop syntax makes awkward, and it makes concrete what the `for range` form is sugar for. ## `Seq` versus `Seq2` `Seq2[K, V]` is identical in spirit; its `yield` takes two arguments and delivers both in a single call — an index and an element, a key and a value, a page number and a decoded page. The names `K` and `V` are just type-parameter names and carry no requirement that the first value be a key, be unique, or be comparable. ## The names are a convention, not magic The compiler does not look at the type's *name* when it compiles a `for range` over a function. It looks at the **shape**: `func(func() bool)`, `func(func(V) bool)` or `func(func(K, V) bool)`. A locally declared `type Lines func(yield func(string) bool)`, or even a bare unnamed `func(func(string) bool)`, ranges exactly the same way, with no import of `iter` anywhere. So why do the named types exist? For agreement. When every package that produces a sequence writes `iter.Seq[T]` in its signature, callers read one spelling, documentation links to one place, and generic helpers can accept sequences from unrelated libraries without conversion. The value of `iter.Seq` is API vocabulary, not compiler behaviour. ## What the type does *not* promise Because `Seq` is only a function type, its guarantees stop at the signature. It does not promise that the sequence is finite, that ranging it twice will produce the same elements — or any elements at all the second time — that it is safe to call from two goroutines at once, or that any resource it opened will be released if the traversal is abandoned. Those are properties of the particular function you were handed, and they belong in the producing API's documentation. ## What to say in an interview "`iter.Seq[V]` is `func(yield func(V) bool)`. An iterator in Go is a function that pushes values into a callback; `for v := range seq` compiles the loop body into that callback, and `yield`'s `bool` result is how the loop says whether it wants more. `Seq2` is the two-value version. The names are a shared convention — the compiler matches on the function shape, not on the `iter` package."

  • Do you need to import `iter` for a `for range` loop over a function to compile?
    No. The range statement works on any function with the right shape — `func(func() bool)`, `func(func(V) bool)` or `func(func(K, V) bool)` — and the compiler never inspects the type's name. `iter.Seq` and `iter.Seq2` exist so that unrelated packages spell the same idea the same way; using them is an API convention, not a compiler requirement.
  • What is the difference between `iter.Seq[V]` and `iter.Seq2[K, V]`?
    `Seq` hands the loop one value per iteration; `Seq2`'s yield is `func(K, V) bool`, so it hands over two at once. The pair is whatever the producer finds useful together — an index and an element, a page number and a page. Both values arrive from a single `yield` call, so `Seq2` is not two sequences being zipped as you consume them.
  • Is an `iter.Seq[V]` value safe to range over from two goroutines at the same time?
    The type gives you no such guarantee. Ranging it twice concurrently calls the same function twice concurrently; whether that is safe depends entirely on what the function touches. One that closes over a shared counter, decoder or open file will race. Treat concurrency safety as something the producing API must document, exactly like any other shared function value.

A pull iterator is a vending machine you press for each item. A push iterator is a caterer: you hand over a tray and they load it, item after item, until you say stop.

saying these in an interview costs you the question

  • Calls iter.Seq an interface with a Next method
  • Thinks a Seq value stores a position between iterations
  • Says you must import iter for range over a function to compile
  • Believes yield returns the next element to the loop
  • Assumes a Seq buffers all elements before the loop starts
open as a page

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%

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.

open as a page

What does `iter.Seq2[K, V]` yield, and when would you return one instead of `iter.Seq`?

level: middleimportance: should knowfreq 45%

basics

~20 s

iter.Seq2[K, V] is func(yield func(K, V) bool): each yield call delivers two related values at once. Return one when every element has a companion the caller would otherwise have to reconstruct — an index, a key, a page number.

open as a page

An SDK's `iter.Seq[Item]` yields 200 items on the first range and nothing on a second. Why?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

Nothing in iter.Seq's contract makes a sequence restartable. This one closes over a drained response body or a spent page cursor, so the second range calls it again, it yields nothing and returns normally: an empty result, not an error.

open as a page