Skip to content

Iterators

Before Go 1.23, a function that produced a sequence had three awkward choices: build and return a whole slice (allocates everything up front), hand out a channel (spawns a goroutine and is slow), or invent a custom Next() type that nobody’s range loop understood. Go 1.23 added range-over-func: for can range over a function of a specific shape, and the standard library gained the iter package to name those shapes.

The result is lazy, allocation-free sequences that work with the loop syntax you already know.

The iter package defines two generic function types:

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

An iterator is a function that receives a yield callback. It calls yield once per element; yield returns false when the consumer wants to stop, and the iterator must return immediately in that case. Seq2 yields pairs — index and value, or key and value — mirroring the two-variable range form.

Here is a counter that yields 0..n-1 and stops early when asked:

func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return // consumer broke out of the loop
}
}
}
}
for v := range Count(5) {
if v == 3 {
break // yield returns false inside Count, which returns
}
fmt.Println(v)
}

break, return, and continue inside the loop body all do what you expect: the compiler turns the body into the yield function and translates control flow for you. The iterator only sees yield returning false.

Because the sequence is produced on demand, Count(1_000_000_000) costs nothing until you iterate — and if you stop after three items, only three are ever produced.

Reusing an iterator after early exit panics

Section titled “Reusing an iterator after early exit panics”

The yield == false rule from the previous section is not a suggestion — an iterator that ignores it and keeps calling yield after it has already returned false once is a runtime error, not just a logic bug:

func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
yield(i) // BUG: ignores yield's return value and keeps going
}
}
}

Verified on 1.26.5, calling this and breaking out of the range loop early panics:

panic: runtime error: range function continued iteration after function for loop body returned false

The Go runtime tracks whether the loop body already asked to stop and enforces it — every hand-written iterator must check yield’s return value and return immediately when it is false, exactly as Count does earlier in this lesson.

A defer written inside the body of a for ... range someIterator loop does not run at the end of each iteration — it runs when the enclosing function returns, because the loop body is compiled into a closure that the iterator calls repeatedly, and every defer inside that closure is scoped to the function containing the for loop, not to the closure itself:

func process() {
for v := range Count(3) {
defer fmt.Println("deferred for", v) // queues up, doesn't run yet
fmt.Println("processing", v)
}
fmt.Println("loop finished")
}

Verified on 1.26.5, this prints all three "processing" lines and "loop finished" before any "deferred for" line — the three deferred calls accumulate across iterations and only run, in LIFO order, when process itself returns. Code that expects defer inside a range-over-func loop to behave like defer at the end of a single iteration (as if it were an ordinary for loop body) will be surprised: use an explicit helper function per iteration, or call cleanup directly instead of defer, if you need it to happen every time around the loop.

The idiomatic shape is a method named All, Keys, Values, or similar that returns an iterator instead of a slice:

type List[T any] struct{ items []T }
func (l *List[T]) All() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range l.items {
if !yield(i, v) {
return
}
}
}
}
for i, v := range list.All() {
fmt.Println(i, v)
}

Callers never touch the backing slice, and you are free to change the representation later — a linked list, a database cursor, a paged HTTP API — without changing the loop.

Nothing in iter.Seq’s type forces a sequence to support being ranged over more than once — that depends entirely on what the closure does. List.All() above is re-iterable: every call produces a fresh closure that reads l.items from the start. A sequence backed by something that gets consumed as it’s read — a channel, a network connection, a bufio scanner — is single-pass: once drained, ranging over the same iter.Seq value again yields nothing at all, silently:

func FromChannel(ch <-chan int) iter.Seq[int] {
return func(yield func(int) bool) {
for v := range ch {
if !yield(v) {
return
}
}
}
}

Verified on 1.26.5: ranging over the same seq := FromChannel(ch) twice prints the channel’s values on the first pass and prints nothing on the second — there is no error, no panic, just an empty loop, because the channel has already been drained. When a function returns an iter.Seq, nothing in its type tells you whether it’s safe to range over twice; check the doc comment or the source, the same way you would for a function returning a *bufio.Reader.

Since Go 1.23 the slices and maps packages (and strings/bytes since Go 1.24) both produce and consume iterators:

import ("maps"; "slices"; "strings")
// consume: turn an iterator into a slice
evens := slices.Collect(filter(Count(10), isEven)) // []int
keys := slices.Sorted(maps.Keys(m)) // sorted keys, one line
// produce: iterate without copying
for i, v := range slices.All(xs) { /* index, value */ }
for v := range slices.Values(xs) { /* value only */ }
for k, v := range maps.All(m) { /* key, value */ }
// strings (Go 1.24): lazy splitting
for line := range strings.Lines(text) { /* each line, newline kept */ }
for part := range strings.SplitSeq(csv, ",") { /* no []string allocated */ }

slices.Collect, slices.Sorted, slices.SortedFunc, maps.Collect, and maps.Insert are the bridges back to concrete data. strings.SplitSeq and strings.FieldsSeq are the lazy twins of Split and Fields.

A few more helpers worth knowing by name:

FunctionProduces
slices.Backward(s)iter.Seq2[int, E] — index/value pairs, descending indices
slices.Chunk(s, n)iter.Seq[Slice] — consecutive sub-slices of up to n elements
maps.Values(m)iter.Seq[V] — values only, order unspecified
maps.Keys(m)iter.Seq[K] — keys only, order unspecified

maps.Keys returns an iter.Seq, not a slice

Section titled “maps.Keys returns an iter.Seq, not a slice”

Before Go 1.23, maps.Keys (in golang.org/x/exp/maps) returned a []K. The standard library version returns iter.Seq[K] instead — a function, not a slice — so code written against the old shape breaks at compile time rather than silently misbehaving:

m := map[string]int{"z": 1, "a": 2}
sort.Strings(maps.Keys(m)) // BUG: maps.Keys returns iter.Seq[string], not []string

Verified on 1.26.5, this is a compile error:

cannot use maps.Keys(m) (value of func type iter.Seq[string]) as []string value in argument to sort.Strings

The fix already appears earlier in this lesson: slices.Sorted(maps.Keys(m)) collects the iterator into a sorted []string in one call, replacing the old two-step “collect into a slice, then sort.Strings” pattern entirely.

A tiny filter for reference — combinators like this compose naturally:

func filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}

Range-over-func is a push model: the producer drives, the consumer’s loop body is a callback. When you need to pull one element at a time — merging two sorted sequences, for example — iter.Pull converts a push iterator into a next()/stop() pair:

next, stop := iter.Pull(Count(3))
defer stop()
v, ok := next() // 0, true

iter.Pull uses a coroutine under the hood, so it is slower than a plain range; reach for it only when the pull shape is genuinely required.

flowchart LR
  subgraph Push["push (for ... range seq)"]
    Producer["iterator drives"] --> Body["loop body runs as yield callback"]
  end
  subgraph Pull["pull (iter.Pull)"]
    Caller["caller drives"] -->|"next()"| Coroutine["iterator's coroutine resumes to the next yield"]
    Coroutine -->|"value, true"| Caller
    Caller -->|"stop()"| Coroutine
    Coroutine -->|"releases resources"| Done["coroutine exits"]
  end
Push: the iterator calls you. Pull: you call the iterator, and must call stop() when done

The official contract, from the iter package doc: stop “must be called when the caller is no longer interested in next values and next has not yet signaled that the sequence is over.” In practice, iter.Pull runs the original push iterator on a coroutine that suspends itself between values; stop() is what tells that coroutine to unwind and release whatever it holds — without it, a next() sequence you abandon partway through leaves the coroutine parked indefinitely. It is always valid to call stop more than once, and valid to call next after stop (it keeps returning the zero value and false) — which is exactly why defer stop() immediately after iter.Pull is the idiomatic pattern, the same reflex as defer resp.Body.Close() or defer f.Close().

  • Return a slice when the data is already a slice and small. An iterator adds nothing there.
  • Return an iterator when the sequence is large, unbounded, expensive to produce, backed by I/O, or when you want to hide the representation.
  • Always honour yield == false. An iterator that keeps calling yield after it returned false panics.
  • Name accessors All, Keys, Values, Backward to match the standard library.
  • Iterators are plain functions: they are not goroutine-safe by magic, and they hold whatever the closure captured.

Everything above in one runnable program — Count, filter, List.All, and the slices/maps bridges:

package main
import (
"fmt"
"iter"
"maps"
"slices"
)
// Count yields 0, 1, …, n-1 lazily.
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return // the consumer broke out of its loop
}
}
}
}
func filter[T any](seq iter.Seq[T], keep func(T) bool) iter.Seq[T] {
return func(yield func(T) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
type List[T any] struct{ items []T }
func (l *List[T]) All() iter.Seq2[int, T] {
return func(yield func(int, T) bool) {
for i, v := range l.items {
if !yield(i, v) {
return
}
}
}
}
func main() {
isEven := func(n int) bool { return n%2 == 0 }
fmt.Println(slices.Collect(filter(Count(10), isEven)))
for v := range Count(100) {
if v > 2 {
break // Count returns as soon as yield reports false
}
fmt.Println("got", v)
}
list := &List[string]{items: []string{"a", "b", "c"}}
for i, v := range list.All() {
fmt.Println(i, v)
}
m := map[string]int{"z": 26, "a": 1, "m": 13}
fmt.Println(slices.Sorted(maps.Keys(m)))
}

When a generated snippet writes or consumes an iterator, check for these before trusting it:

  • Does a hand-written iterator ignore yield’s return value and keep looping after it returns false? That’s a runtime panic, not a silent bug.
  • Is sort.Strings, sort.Ints, or similar called directly on the result of maps.Keys/maps.Values/slices.Values? Those return an iter.Seq, not a slice — it won’t compile.
  • Is defer inside a range-over-func loop body assumed to run once per iteration, for cleanup that must happen every time around the loop?
  • Does code range over the same iter.Seq value a second time, expecting the same results, when the underlying source (a channel, a scanner, a network read) can only be drained once?
  • Is iter.Pull used without defer stop() on every return path, including early returns and errors?

🐛 Spot the bug

func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
yield(i)
}
}
}
func main() {
for v := range Count(5) {
if v == 2 {
break
}
fmt.Println(v)
}
}
Show the answer

Count’s iterator calls yield(i) but never checks the return value, so it keeps calling yield on every remaining i even after the loop already broke out. Verified on 1.26.5, this panics: panic: runtime error: range function continued iteration after function for loop body returned false. The fix is if !yield(i) { return }, exactly as the correct Count earlier in this lesson does.

package main
import (
"fmt"
"iter"
)
func Count(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i < n; i++ {
if !yield(i) {
return
}
}
}
}
// process shows that defer inside a range-over-func body queues up for the
// whole enclosing function, not per iteration -- it runs LIFO when process
// itself returns.
func process() {
for v := range Count(3) {
defer fmt.Println("deferred for", v)
fmt.Println("processing", v)
}
fmt.Println("loop finished")
}
// FromChannel is single-pass: once the channel is drained, ranging over the
// same Seq again yields nothing.
func FromChannel(ch <-chan int) iter.Seq[int] {
return func(yield func(int) bool) {
for v := range ch {
if !yield(v) {
return
}
}
}
}
func main() {
process()
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
seq := FromChannel(ch)
fmt.Println("first pass:")
for v := range seq {
fmt.Println(v)
}
fmt.Println("second pass on the same (now-drained) sequence:")
for v := range seq {
fmt.Println(v)
}
fmt.Println("second pass produced nothing -- the channel has no more values")
// iter.Pull for manual, one-at-a-time control.
next, stop := iter.Pull(Count(3))
defer stop() // required: releases the iterator's underlying coroutine
for {
v, ok := next()
if !ok {
break
}
fmt.Println("pulled", v)
}
}
What is the definition of iter.Seq[V]?
Does this compile?
sort.Strings(maps.Keys(m))
What happens when this runs?
func Count(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            yield(i)
        }
    }
}

for v := range Count(5) {
    if v == 2 {
        break
    }
}
What happens on the second range?
seq := FromChannel(ch) // reads from a channel until it's closed
for v := range seq { /* ... */ }
for v := range seq { /* ... */ } // ranging again
Why should defer stop() follow every call to iter.Pull?
When is returning a plain slice the better design?