Iterators
An iterator produces a sequence of values one at a time, so a for … range loop can walk through data without first building a slice of all of it. In Go, an iterator is simply a function that calls a yield callback for each value, and range accepts such functions directly. This lesson covers iter.Seq and iter.Seq2, writing your own iterators, the standard library's iterators, and composing them.
Ranging over a function
A function with the signature func(yield func(V) bool) can be used in a range loop. Each call to yield runs one pass of the loop body; yield returns false if the loop has stopped early (with break or return), and the iterator must then stop:
package main import "fmt" func countdown(from int) func(yield func(int) bool) { return func(yield func(int) bool) { for i := from; i >= 0; i-- { if !yield(i) { return // the loop body said stop } } } } func main() { for n := range countdown(3) { fmt.Print(n, " ") } fmt.Println() for n := range countdown(100) { if n < 97 { break // yield returns false and the iterator returns } fmt.Print(n, " ") } fmt.Println() }
3 2 1 0 100 99 98 97
iter.Seq and iter.Seq2
The iter package names the two common shapes: iter.Seq[V] yields single values and iter.Seq2[K, V] yields pairs, like the index and value of a slice. Using these names makes signatures readable and lets your iterators work with library functions:
package main import ( "fmt" "iter" ) func Fibonacci() iter.Seq[int] { // infinite: the caller decides when to stop return func(yield func(int) bool) { a, b := 0, 1 for { if !yield(a) { return } a, b = b, a+b } } } func Enumerate[T any](items []T) iter.Seq2[int, T] { return func(yield func(int, T) bool) { for i, v := range items { if !yield(i+1, v) { // numbered from 1 return } } } } func main() { for f := range Fibonacci() { if f > 50 { break } fmt.Print(f, " ") } fmt.Println() for n, s := range Enumerate([]string{"wake up", "make coffee"}) { fmt.Printf("%d. %s\n", n, s) } }
0 1 1 2 3 5 8 13 21 34 1. wake up 2. make coffee
An infinite sequence is fine, because nothing is computed until the loop asks for the next value.
Iterators in the standard library
| Function | Yields |
|---|---|
slices.All(s), slices.Values(s), slices.Backward(s) | Index/value pairs, values, pairs from the end |
maps.Keys(m), maps.Values(m), maps.All(m) | A map's keys, values, or pairs |
strings.Lines, strings.SplitSeq, strings.FieldsSeq | Pieces of a string |
bytes.Lines, bytes.SplitSeq, … | The same for byte slices |
slices.Chunk(s, n) | Sub-slices of up to n elements |
And these collect an iterator's values: slices.Collect, slices.Sorted, slices.AppendSeq, maps.Collect and maps.Insert:
package main import ( "fmt" "maps" "slices" "strings" ) func main() { prices := map[string]float64{"tea": 2.5, "cake": 3.75, "juice": 3} fmt.Println(slices.Sorted(maps.Keys(prices))) words := slices.Collect(strings.FieldsSeq(" iterators are lazy ")) fmt.Println(words, len(words)) index := maps.Collect(slices.All([]string{"zero", "one", "two"})) // map[int]string fmt.Println(index[2]) }
[cake juice tea] [iterators are lazy] 3 two
Building pipelines
Functions that take an iterator and return a new one can be chained into a pipeline. Each value flows through all the steps before the next value is produced, so no intermediate slices are built:
package main import ( "fmt" "iter" "slices" "strings" ) func Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] { return func(yield func(V) bool) { for v := range seq { if keep(v) && !yield(v) { return } } } } func Map[V, W any](seq iter.Seq[V], f func(V) W) iter.Seq[W] { return func(yield func(W) bool) { for v := range seq { if !yield(f(v)) { return } } } } func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] { return func(yield func(V) bool) { if n <= 0 { return } count := 0 for v := range seq { if !yield(v) { return } count++ if count == n { return } } } } func main() { log := "INFO start\nERROR disk full\nINFO retry\nERROR timeout\nERROR disk full\n" lines := Map(strings.Lines(log), strings.TrimSpace) errs := Filter(lines, func(s string) bool { return strings.HasPrefix(s, "ERROR") }) firstTwo := Take(errs, 2) fmt.Println(slices.Collect(firstTwo)) }
[ERROR disk full ERROR timeout]
Because Take stops after two values, the rest of the log is never even split into lines. This laziness is what makes iterators efficient for large files and streams.
Iterators over your own types
A common pattern is an All method that lets callers range over a collection without seeing how it's stored:
package main import ( "fmt" "iter" ) type Tree struct { Left, Right *Tree Value int } func (t *Tree) Insert(v int) *Tree { if t == nil { return &Tree{Value: v} } if v < t.Value { t.Left = t.Left.Insert(v) } else { t.Right = t.Right.Insert(v) } return t } // All yields the values in sorted order (an in-order walk). func (t *Tree) All() iter.Seq[int] { return func(yield func(int) bool) { t.walk(yield) } } func (t *Tree) walk(yield func(int) bool) bool { if t == nil { return true } return t.Left.walk(yield) && yield(t.Value) && t.Right.walk(yield) } func main() { var root *Tree for _, v := range []int{50, 30, 70, 20, 40, 60} { root = root.Insert(v) } for v := range root.All() { fmt.Print(v, " ") if v >= 50 { break } } fmt.Println() }
20 30 40 50
Recursion with && makes the early stop propagate: once yield returns false, every level returns false without visiting anything else.
Pull iterators
A range loop pushes values into the loop body. Occasionally you need to pull values one at a time instead — for example, to merge two sequences. iter.Pull converts a sequence into a next function (and a stop function you must call when finished):
package main import ( "fmt" "iter" "slices" ) func main() { next, stop := iter.Pull(slices.Values([]string{"a", "b", "c"})) defer stop() first, ok := next() fmt.Println(first, ok) second, _ := next() fmt.Println(second) }
a true b
Try it yourself
Write an iterator Range(start, end, step int) iter.Seq[int], then use it to print the multiples of 7 below 50.
0 7 14 21 28 35 42 49