skip to content

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