Range-over-func iterators

Go · Advanced · 14 min

Iterators give custom collections and lazy sequences the same for range syntax as slices and maps.

Go 1.23 lets for range loop over functions. An iterator is a function that takes a yield callback and calls it once per element:

func(yield func(V) bool)       // iter.Seq[V]
func(yield func(K, V) bool)    // iter.Seq2[K, V]

The iter package names these types. When you write for v := range seq { ... }, the compiler turns the loop body into the yield function. yield returns false when the loop stops early (via break, return or a panic), and the iterator must stop calling it then — calling yield after it returned false panics at run time. The standard shape is:

if !yield(v) { return }

These are push iterators: the sequence drives the loop. iter.Pull converts one into a pull-style next()/stop() pair when you need to step through two sequences in lockstep; always call stop when done.

The standard library embraces them:

  • slices.All, slices.Values, slices.Backward, slices.Collect, slices.Sorted.
  • maps.Keys, maps.Values, maps.All, maps.Collect.
  • Go 1.24 added strings.Lines, strings.SplitSeq and friends.

Iterators are lazy: nothing is computed until the loop asks, so infinite sequences and pipelines that stop early are cheap. Their natural partner is generics: functions like Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] compose cleanly.

A method that returns an iterator is conventionally named All, or after what it yields (Keys, Lines). Most iterators can be ranged over more than once; document it if yours can't.

Example

package main

import (
	"fmt"
	"iter"
	"maps"
	"slices"
)

func Fib() iter.Seq[int] { // infinite, lazy
	return func(yield func(int) bool) {
		a, b := 0, 1
		for {
			if !yield(a) {
				return // the loop stopped: so do we
			}
			a, b = b, a+b
		}
	}
}

func main() {
	for n := range Fib() {
		if n > 50 {
			break // yield returns false
		}
		fmt.Print(n, " ") // 0 1 1 2 3 5 8 13 21 34
	}
	fmt.Println()

	ages := map[string]int{"bo": 4, "al": 7}
	fmt.Println(slices.Sorted(maps.Keys(ages))) // [al bo]
}

Remember

  • iter.Seq[V] is func(yield func(V) bool); range over it directly
  • Stop as soon as yield returns false
  • slices and maps provide All/Values/Keys/Collect/Sorted helpers

Watch out for

  • Ignoring yield's return value — breaking out of the loop then causes a runtime panic.

Check yourself

  1. A consumer `break`s out of `for v := range seq`. What does seq's yield call return at that point?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/blog/range-functions