Iterators and Range-over-Func

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18; every go.mod shown has the line go 1.27. Until Go 1.23 the for ... range statement worked only on a fixed list of built-in things: arrays, slices, strings, maps, channels and (since Go 1.22) integers. A library that offered its own collection, a tree, a set or a database result, had to invent a cursor type (for it.Next() { it.Value() }), pass a callback (tree.Walk(func(k K) {...})) or copy everything into a slice first, and none of these could use break, continue or return the way a for loop can. Go 1.23 closed that gap with range over function types: a function with a special shape can be the operand of range, and the body of the loop becomes a callback that the function calls. The result is called an iterator. The language change is tiny, the iter package defines two named types, and the slices, maps, strings, bytes and reflect packages now return iterators from many of their functions.

This page explains the three function shapes, how the compiler turns a loop body into a yield function, the contract an iterator must obey, what happens when it does not, pull iterators (iter.Pull) for the cases where the caller must drive the iteration, writing and composing iterators with generics, every iterator-returning function of the standard library, conventions, pitfalls, and the version in which each piece appeared. It builds on Control Flow (the other range forms, labels, break), Functions, Closures and Defer (function values and defer) and Generics (type parameters, used by every adapter here). The data structures that iterators walk are covered in Arrays and Slices and Maps. Cross-language comparisons are in the Rust Reference (pull-based Iterator with adapters), Python (generators), Java streams and C# iterators (yield return). Go’s version is a form of internal iteration: the loop body is passed to the sequence as a function, not the other way round.

Every example was compiled, vetted with go vet and run with Go 1.27.2; the output shown is the real output (the compiler messages are likewise copied from real runs). The go.mod of the example module is:

module example.com/shelf

go 1.27

Why iterators exist

A collection can be walked in two broad ways. External iteration gives the caller a cursor and lets it decide when to advance; internal iteration hands the collection a function and lets the collection decide how to call it. Both have costs in Go:

  • A cursor object (Next() bool, Value() T) works with for, but the author of the collection must store the walk state in a struct, which is painful for recursive structures such as trees, and the cursor leaks if it holds a resource that is never closed.

  • A callback (Walk(func(T))) is easy to write, even recursively, but the caller loses break, continue, return and defer inside the callback: those statements would apply to the callback, not to the surrounding function.

  • Channels and goroutines turn any producer into a sequence, but each element crosses a synchronisation point, the producer goroutine leaks if the consumer stops early, and the pattern is heavy for something as simple as walking a slice.

Range over function types keeps the simple shape of the callback, so the iterator author writes an ordinary loop (even a recursive one), and gives the caller a real for statement. The compiler rewrites the loop body into a function and rewrites break, continue, goto, labelled jumps, return and defer inside it so that they keep their meaning. The design rationale is in the Range Over Function Types blog post and the range-over-func wiki page.

The three function shapes

The specification (For statements with range clause) accepts a function value as the range expression when it has one of three types. The function is called the iterator; its single parameter, conventionally named yield, is the yield function:

Iterator type Iteration variables Named type in package iter

func(yield func() bool)

none: for range f

none (no named type)

func(yield func(V) bool)

one: for v := range f

iter.Seq[V]

func(yield func(K, V) bool)

two: for k, v := range f

iter.Seq2[K, V]

The iterator must not have any results, and yield must return exactly one bool. The number of iteration variables may be smaller than the number of yield parameters (you may write for k := range seq2 or for range seq2) but never larger. The element types V, K and V can be anything, including other functions or iter.Seq values.

package main

import "fmt"

// Count calls yield with 1, 2, ... n. It is a push iterator.
func Count(n int) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		for i := 1; i <= n; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

// Pairs yields index/title pairs.
func Pairs(titles []string) func(yield func(int, string) bool) {
	return func(yield func(int, string) bool) {
		for i, t := range titles {
			if !yield(i, t) {
				return
			}
		}
	}
}

// Ticks yields nothing but signals n times.
func Ticks(n int) func(yield func() bool) {
	return func(yield func() bool) {
		for range n {
			if !yield() {
				return
			}
		}
	}
}

func main() {
	for v := range Count(3) {
		fmt.Println("value", v)
	}
	for i, t := range Pairs([]string{"Dune", "Emma"}) {
		fmt.Println("pair", i, t)
	}
	for range Ticks(2) {
		fmt.Println("tick")
	}
	// Count is an ordinary function: it can also be called directly.
	Count(2)(func(v int) bool {
		fmt.Println("direct", v)
		return true
	})
}
value 1
value 2
value 3
pair 0 Dune
pair 1 Emma
tick
tick
direct 1
direct 2

Count is a normal function. Ranging over it does not call it in any special way: the compiler calls Count(3) once to obtain the iterator, then calls the iterator with a synthesised yield. The last lines of main show that the iterator can also be called by hand with any function of the right shape, which is exactly what happens when the loop is rewritten.

Two details of the rules:

  • As with every for loop, the iteration variables declared with := are new for each iteration (the Go 1.22 rule, see Control Flow); a closure created in the body captures that iteration’s value, not a variable shared by all iterations.

  • Range over function types, like range over integers, depends on the go line of the module: a module that says go 1.22 or older cannot use it (see the compile errors below).

How a loop body becomes yield

Conceptually, the statement

for v := range seq {
	body
}

means "call seq with a function that runs body for one value and reports whether to go on". The specification describes it as follows (paraphrased): the iteration proceeds by calling the function with a new, synthesised yield; every call of yield assigns its arguments to the iteration variables and runs the body once; if the body finishes normally (or with continue), yield returns true and may be called again; if the body terminates the loop (with break, return, a goto to a label outside, a labelled continue of an outer loop or a panic) yield returns false and must not be called again; when the function returns, the loop is over.

sequenceDiagram participant F as enclosing function participant I as iterator seq participant Y as yield (the loop body) F->>I: seq(yield) activate I I->>Y: yield(v1) Y-->>I: true (body finished or continue) I->>Y: yield(v2) Note over Y: body runs break, return or goto Y-->>I: false Note over I: must return now, deferred calls run I-->>F: seq returns deactivate I Note over F: the jump (break, return, label) is completed here

The diagram makes the key point of the design visible: the loop body runs inside the iterator’s call stack, between two statements of the iterator. This is why the contract in the next sections matters: when the body wants to leave the loop, it cannot unwind the iterator’s stack itself; it returns false and relies on the iterator to return promptly, after which the compiler-generated code performs the real break or return in the enclosing function.

iter.Seq and iter.Seq2

The iter package (pkg.go.dev/iter, Go 1.23) contains the two named generic types and the pull functions. Its whole API is:

Declaration Meaning

type Seq[V any] func(yield func(V) bool)

a sequence of single values

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

a sequence of pairs, usually key-value or index-value

func Pull[V any](seq Seq[V]) (next func() (V, bool), stop func())

convert a push iterator into a pull iterator

func Pull2[K, V any](seq Seq2[K, V]) (next func() (K, V, bool), stop func())

the same for pairs

The named types are defined function types, but a function literal or any function with the underlying signature is assignable to them, so an iterator can be written without ever naming iter.Seq. The advantage of the names is readability and type inference: a signature func Filter[V any](seq iter.Seq[V], ...) iter.Seq[V] reads better than func Filter[V any](seq func(yield func(V) bool), ...) func(yield func(V) bool), and the compiler can infer V from either form.

Producers conventionally return an iter.Seq; consumers accept one. The shelf catalogue below shows the typical shapes: a method named All for every element, a variant with its own name for a filtered or reversed walk, and an iterator built on top of another iterator.

package main

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

type ISBN string

type Book struct {
	ISBN   ISBN
	Title  string
	Author string
	Year   int
}

// Catalog is a small in-memory collection of books.
type Catalog struct {
	books []Book
}

func (c *Catalog) Add(b Book) { c.books = append(c.books, b) }

// All returns an iterator over every book in insertion order.
func (c *Catalog) All() iter.Seq[Book] {
	return func(yield func(Book) bool) {
		for _, b := range c.books {
			if !yield(b) {
				return
			}
		}
	}
}

// Backward returns an iterator over every book, newest insertion first.
func (c *Catalog) Backward() iter.Seq2[int, Book] {
	return func(yield func(int, Book) bool) {
		for i := len(c.books) - 1; i >= 0; i-- {
			if !yield(i, c.books[i]) {
				return
			}
		}
	}
}

// ByAuthor returns an iterator over the books written by author.
func (c *Catalog) ByAuthor(author string) iter.Seq[Book] {
	return func(yield func(Book) bool) {
		for b := range c.All() {
			if b.Author == author && !yield(b) {
				return
			}
		}
	}
}

func main() {
	var c Catalog
	c.Add(Book{"9780441172719", "Dune", "Frank Herbert", 1965})
	c.Add(Book{"9780441172726", "Dune Messiah", "Frank Herbert", 1969})
	c.Add(Book{"9780441478125", "The Left Hand of Darkness", "Ursula K. Le Guin", 1969})

	for b := range c.All() {
		fmt.Println(b.Title)
	}
	for i, b := range c.Backward() {
		fmt.Println(i, b.Title)
	}
	for b := range c.ByAuthor("Frank Herbert") {
		fmt.Println("herbert:", b.Year, b.Title)
	}
	titles := slices.Collect(c.ByAuthor("Frank Herbert"))
	fmt.Println(len(titles), "books collected")
}
Dune
Dune Messiah
The Left Hand of Darkness
2 The Left Hand of Darkness
1 Dune Messiah
0 Dune
herbert: 1965 Dune
herbert: 1969 Dune Messiah
2 books collected

Observations:

  • The iterator is a closure over the receiver c; calling c.All() allocates nothing by itself, the work happens when the loop calls the returned function.

  • ByAuthor ranges over c.All() inside its own body: iterators compose by nesting loops, and the inner yield result is propagated outwards (!yield(b) then return). Forgetting that propagation is the single most common iterator bug.

  • The iterator is evaluated lazily. Nothing happens when ByAuthor("Frank Herbert") is called; the filter runs while the consumer (range or slices.Collect) pulls elements.

  • A Seq2 is not a map: Backward yields an index and a book, in the order the author chose.

  • Because the loop variable of range is per-iteration, the b yielded to the body is a copy of the element, exactly like an ordinary range over a slice of structs. Modifying it does not change the catalogue.

The yield contract and early termination

An iterator must follow these rules (the iter package documentation states that "Yield panics if called after it returns false"; the others follow from the specification):

  1. If yield returns false, the iterator must stop calling it and return as soon as it reasonably can.

  2. The iterator must not call yield after the iterator function has returned. A stored yield is dead once the loop is over.

  3. Calls of yield must happen one at a time, from a single goroutine: the loop body is ordinary sequential code and is not written to be re-entered concurrently.

  4. The iterator may run cleanup code, typically with defer, and the compiler guarantees that this code runs even if the loop body leaves the loop by break, return or a panic.

The next program logs every step so that the order of events is visible, including what break and return cause the iterator to do.

package main

import (
	"fmt"
	"iter"
)

func Count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		defer fmt.Println("  iterator: cleanup")
		for i := 1; i <= n; i++ {
			fmt.Println("  iterator: yielding", i)
			if !yield(i) {
				fmt.Println("  iterator: told to stop")
				return
			}
		}
		fmt.Println("  iterator: exhausted")
	}
}

func first(limit int) int {
	for v := range Count(5) {
		if v == limit {
			return v * 10 // return from the enclosing function
		}
	}
	return -1
}

func main() {
	fmt.Println("full loop")
	for v := range Count(2) {
		fmt.Println("body", v)
	}
	fmt.Println("break")
	for v := range Count(5) {
		if v == 2 {
			break
		}
		fmt.Println("body", v)
	}
	fmt.Println("return from the enclosing function")
	fmt.Println("result", first(2))
}
full loop
  iterator: yielding 1
body 1
  iterator: yielding 2
body 2
  iterator: exhausted
  iterator: cleanup
break
  iterator: yielding 1
body 1
  iterator: yielding 2
  iterator: told to stop
  iterator: cleanup
return from the enclosing function
  iterator: yielding 1
  iterator: yielding 2
  iterator: told to stop
  iterator: cleanup
result 20

Read the output as the sequence diagram above: on break (and equally on return) the iterator sees yield return false, prints "told to stop", runs its deferred call and returns; only then does the enclosing function leave the loop. The return v * 10 in first is a return of the enclosing function, with the right value, even though the statement executes deep inside a callback.

Everything a loop body may do

Because the compiler rewrites the body, all the usual control-flow statements work, including the awkward ones:

  • continue makes yield return true;

  • break makes yield return false, and the loop ends after the iterator returns;

  • return (with or without values) records the results, makes yield return false, and the enclosing function returns when the iterator does;

  • labelled break and continue that target an outer loop, and goto to a label outside the body, are supported across any number of nested range-over-func loops;

  • defer in the body registers the call on the enclosing function, not on the callback, so it runs when that function returns;

  • a panic in the body propagates through the iterator’s stack (running its deferred calls) up to whoever recovers it;

  • recover and runtime.Goexit keep their meaning.

package main

import (
	"fmt"
	"iter"
)

func Count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := range n {
			if !yield(i) {
				return
			}
		}
	}
}

func Letters() iter.Seq[string] {
	return func(yield func(string) bool) {
		for _, s := range []string{"a", "b", "c"} {
			if !yield(s) {
				return
			}
		}
	}
}

func find() string {
outer:
	for i := range Count(3) {
		for l := range Letters() {
			switch {
			case i == 0 && l == "b":
				continue outer // next i
			case i == 1 && l == "c":
				break outer // leave both loops
			case i == 2:
				return "unreachable"
			}
			fmt.Println("visit", i, l)
		}
	}
	return "done"
}

func deferred() {
	for i := range Count(2) {
		defer fmt.Println("deferred in body", i) // runs when deferred returns
	}
	fmt.Println("loop finished")
}

func main() {
	fmt.Println(find())
	deferred()
}
visit 0 a
visit 1 a
visit 1 b
done
loop finished
deferred in body 1
deferred in body 0

continue outer jumps to the next i (so b and c are skipped for i == 0), break outer leaves both loops when i == 1 reaches "c", and the deferred function prints its two deferred lines, in last-in-first-out order, only after the loop has finished and the function is returning. The body of an ordinary range would behave identically.

What goes wrong when an iterator misbehaves

Because yield is an ordinary function, the compiler cannot prevent an iterator from ignoring its result. The runtime detects the most common violations and panics with a runtime.Error. An iterator that ignores false and keeps calling yield is caught on the next call:

package main

import (
	"fmt"
	"iter"
)

// Broken ignores the false result of yield.
func Broken() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; i <= 3; i++ {
			yield(i) // BUG: result ignored
		}
	}
}

func main() {
	defer func() {
		fmt.Println("recovered:", recover())
	}()
	for v := range Broken() {
		fmt.Println("body", v)
		break
	}
}
body 1
recovered: runtime error: range function continued iteration after function for loop body returned false

Source: iter: Iterators

Without the check, the body would have run a second time after break. The same detection applies to an iterator that stores yield and calls it after the loop has ended, even though the loop ended normally:

package main

import (
	"fmt"
	"iter"
)

var saved func(int) bool

// Leaky stores yield so that it can be called after the loop is over.
func Leaky() iter.Seq[int] {
	return func(yield func(int) bool) {
		saved = yield
		yield(1)
	}
}

func main() {
	defer func() {
		fmt.Println("recovered:", recover())
	}()
	for v := range Leaky() {
		fmt.Println("body", v)
	}
	saved(2)
}
body 1
recovered: runtime error: range function continued iteration after whole loop exit

Source: iter: Iterators

The checks are cheap flags set by the code the compiler generates; they cannot catch an iterator that merely takes too long to return after false, nor concurrent calls from several goroutines, which are a data race. go vet has no analyzer for a forgotten yield check, so tests that break out of the loop (see Testing iterators) are the defence. A third run-time failure is a call through a nil iterator, which is an ordinary nil function call:

package main

import (
	"fmt"
	"iter"
)

func main() {
	defer func() { fmt.Println("recovered:", recover()) }()
	var seq iter.Seq[int] // nil
	for v := range seq {
		fmt.Println(v)
	}
}
recovered: runtime error: invalid memory address or nil pointer dereference

Panics and cleanup

A panic raised by the loop body unwinds through the iterator’s frames, so the iterator’s defer statements run before the panic reaches the caller. A panic raised by the iterator itself unwinds through the loop, and the rest of the body of the enclosing function is skipped as for any panic. In both directions deferred cleanup is guaranteed:

package main

import (
	"fmt"
	"iter"
)

func Boom() iter.Seq[int] {
	return func(yield func(int) bool) {
		defer fmt.Println("iterator: deferred cleanup runs")
		yield(1)
		panic("iterator failed")
	}
}

func BodyPanics() iter.Seq[int] {
	return func(yield func(int) bool) {
		defer fmt.Println("iterator: deferred cleanup runs")
		for i := 1; ; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func try(name string, f func()) {
	defer func() { fmt.Println(name, "recovered:", recover()) }()
	f()
}

func main() {
	try("iterator panic", func() {
		for v := range Boom() {
			fmt.Println("body", v)
		}
	})
	try("body panic", func() {
		for v := range BodyPanics() {
			if v == 2 {
				panic("body failed")
			}
		}
	})
}
body 1
iterator: deferred cleanup runs
iterator panic recovered: iterator failed
iterator: deferred cleanup runs
body panic recovered: body failed

This is what makes iterators the right tool for resources: an iterator that opens a file, a database cursor (*sql.Rows) or a lock can release it with defer in the iterator function itself, and every way the loop can end is covered. The caller writes a plain for loop with no Close call. Chapter-length cleanup logic that earlier versions of Go scattered over defer rows.Close() at each call site can now live in one iterator.

Iterators that return errors

An iterator has no place to return an error: its type has no results. Three conventions exist:

  1. a iter.Seq2[T, error] that yields (value, nil) for each element and (zero, err) for a failure, after which the iterator normally stops (the form used in the examples of this page);

  2. an iter.Seq[T] together with an Err() error method on the receiver that the caller reads after the loop (the bufio.Scanner style);

  3. a pointer to an error passed into the iterator constructor.

The first form makes forgetting to check impossible to miss, because the second iteration variable is visible in the loop header; it is the most common choice in new code. The error can also be non-final (the example below continues after a recoverable error) as long as the documentation says so.

package main

import (
	"bufio"
	"errors"
	"fmt"
	"iter"
	"strings"
)

var ErrEmptyTitle = errors.New("empty title")

// Titles reads one title per line. Each element is a value and an error;
// after yielding a non-nil error the iterator stops.
func Titles(r *bufio.Reader) iter.Seq2[string, error] {
	return func(yield func(string, error) bool) {
		for {
			line, err := r.ReadString('\n')
			line = strings.TrimSpace(line)
			if line != "" {
				if !yield(line, nil) {
					return
				}
			} else if err == nil {
				if !yield("", ErrEmptyTitle) {
					return
				}
			}
			if err != nil {
				return // io.EOF: normal end of the sequence
			}
		}
	}
}

func main() {
	in := bufio.NewReader(strings.NewReader("Dune\nEmma\n\nUlysses\n"))
	for title, err := range Titles(in) {
		if err != nil {
			fmt.Println("error:", err)
			continue
		}
		fmt.Println("title:", title)
	}
}
title: Dune
title: Emma
error: empty title
title: Ulysses

See Error Handling for wrapping and sentinel errors.

Infinite, single-use and reusable iterators

An iterator is allowed to be infinite: the loop ends only through break, return or goto, and the iterator returns when yield says false. This is the same shape as a generator in other languages, but there is no hidden state: the state lives in the closure, one fresh copy per call of the iterator.

package main

import (
	"fmt"
	"iter"
)

// Fib is an infinite iterator: it only ends when the loop body breaks.
func Fib() iter.Seq[int] {
	return func(yield func(int) bool) {
		a, b := 0, 1
		for {
			if !yield(a) {
				return
			}
			a, b = b, a+b
		}
	}
}

// Numbered yields 1, 2, 3, ... without end.
func Numbered() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; ; i++ {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	for n := range Fib() {
		if n > 50 {
			break
		}
		fmt.Print(n, " ")
	}
	fmt.Println()
	for n := range Numbered() {
		if n%2 == 0 {
			continue
		}
		if n > 9 {
			break
		}
		fmt.Print(n, " ")
	}
	fmt.Println()
}
0 1 1 2 3 5 8 13 21 34
1 3 5 7 9

Whether an iterator can be ranged over more than once depends on where its state lives. Fib creates a and b inside the returned function, so each loop starts from the beginning: it is reusable. An iterator that consumes an external resource, a bufio.Scanner, a network connection or a channel, is single-use: the second loop sees what is left, which is nothing. The documentation of every iterator-returning function should say which one it is (strings.Lines, for example, is documented as returning a single-use iterator, whereas slices.Values can be ranged over any number of times).

package main

import (
	"bufio"
	"fmt"
	"iter"
	"strings"
)

// Lines returns a single-use iterator: it consumes r, so a second range
// loop over the same value sees nothing.
func Lines(r *bufio.Scanner) iter.Seq[string] {
	return func(yield func(string) bool) {
		for r.Scan() {
			if !yield(r.Text()) {
				return
			}
		}
	}
}

func main() {
	seq := Lines(bufio.NewScanner(strings.NewReader("Dune\nEmma\n")))
	for l := range seq {
		fmt.Println("first pass:", l)
	}
	for l := range seq {
		fmt.Println("second pass:", l)
	}
	fmt.Println("second pass is empty")
}
first pass: Dune
first pass: Emma
second pass is empty

Recursive iterators: walking a tree

Recursion is where iterators beat both cursors and callbacks. A cursor over a binary tree needs an explicit stack; a callback walk loses break. With an iterator, the recursive helper returns the bool produced by yield and every level propagates it, so a break at depth ten unwinds all ten calls immediately:

package main

import (
	"cmp"
	"fmt"
	"iter"
	"slices"
)

// Tree is a binary search tree.
type Tree[K cmp.Ordered] struct {
	left, right *Tree[K]
	key         K
}

// Insert adds key and returns the (possibly new) root.
func (t *Tree[K]) Insert(key K) *Tree[K] {
	if t == nil {
		return &Tree[K]{key: key}
	}
	switch c := cmp.Compare(key, t.key); {
	case c < 0:
		t.left = t.left.Insert(key)
	case c > 0:
		t.right = t.right.Insert(key)
	}
	return t
}

// All yields the keys in ascending order. The recursive helper returns false
// as soon as yield does, so the whole recursion unwinds at once.
func (t *Tree[K]) All() iter.Seq[K] {
	return func(yield func(K) bool) {
		t.push(yield)
	}
}

func (t *Tree[K]) push(yield func(K) bool) bool {
	if t == nil {
		return true
	}
	return t.left.push(yield) && yield(t.key) && t.right.push(yield)
}

func main() {
	var root *Tree[string]
	for _, s := range []string{"Hamlet", "Dune", "Ulysses", "Emma", "Dracula"} {
		root = root.Insert(s)
	}
	fmt.Println(slices.Collect(root.All()))
	for k := range root.All() {
		fmt.Println("first key:", k)
		break
	}
}
[Dracula Dune Emma Hamlet Ulysses]
first key: Dracula

The expression t.left.push(yield) && yield(t.key) && t.right.push(yield) is the idiom: short-circuit evaluation stops the walk the moment any part returns false. Note that Insert on a nil receiver works because methods may be called on nil pointers (see Methods and Receivers).

Composing iterators: adapters with generics

Because an iterator is a value, functions can take one and return another. The usual adapters, Filter, Map, Take, Enumerate, Zip, Chain, are each a dozen lines of generic code. The standard library does not ship them: the iter package contains only the two types and Pull/Pull2, and the helpers are the ones listed in the tables below. The adapters below are the ones to copy into an internal package, and in many cases a plain for loop with an if is clearer than a chain of adapters.

package main

import (
	"fmt"
	"iter"
	"slices"
	"strings"
)

// Filter yields the elements of seq for which keep returns true.
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
			}
		}
	}
}

// Map yields f(v) for every v in seq.
func Map[V, U any](seq iter.Seq[V], f func(V) U) iter.Seq[U] {
	return func(yield func(U) bool) {
		for v := range seq {
			if !yield(f(v)) {
				return
			}
		}
	}
}

// Take yields at most n elements of seq.
func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
	return func(yield func(V) bool) {
		if n <= 0 {
			return
		}
		i := 0
		for v := range seq {
			if !yield(v) {
				return
			}
			i++
			if i >= n {
				return
			}
		}
	}
}

// Enumerate turns a Seq into a Seq2 of index and value.
func Enumerate[V any](seq iter.Seq[V]) iter.Seq2[int, V] {
	return func(yield func(int, V) bool) {
		i := 0
		for v := range seq {
			if !yield(i, v) {
				return
			}
			i++
		}
	}
}

// Keys and Values split a Seq2 again.
func Keys[K, V any](seq iter.Seq2[K, V]) iter.Seq[K] {
	return func(yield func(K) bool) {
		for k := range seq {
			if !yield(k) {
				return
			}
		}
	}
}

func Numbers() iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 1; ; i++ {
			fmt.Println("  produce", i)
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	titles := []string{"dune", "emma", "ulysses", "dracula", "hamlet"}
	long := Filter(slices.Values(titles), func(s string) bool { return len(s) > 4 })
	upper := Map(long, strings.ToUpper)
	for i, t := range Enumerate(upper) {
		fmt.Println(i, t)
	}
	fmt.Println(slices.Collect(Keys(Enumerate(slices.Values(titles)))))

	// Laziness: nothing runs until a loop pulls, and Take stops the producer.
	squares := Map(Filter(Numbers(), func(n int) bool { return n%2 == 1 }), func(n int) int { return n * n })
	fmt.Println("first three odd squares:", slices.Collect(Take(squares, 3)))
}
0 ULYSSES
1 DRACULA
2 HAMLET
[0 1 2 3 4]
  produce 1
  produce 2
  produce 3
  produce 4
  produce 5
first three odd squares: [1 9 25]

Two things deserve attention. First, laziness: the pipeline Map(Filter(Numbers(), odd), square) is only a nested set of closures until slices.Collect drives it, and Take returns as soon as it has three elements, so Numbers is asked to produce exactly the five values it takes to find three odd numbers and then stops. Without the n check in Take before the loop (and the early return after the last element) the producer would run one value further than necessary. Second, every adapter has the same skeleton: for v := range input { ... if !yield(out) { return } }. Keep that skeleton intact.

flowchart LR src["Numbers()
1, 2, 3, 4, 5 ..."] --> flt["Filter odd"] flt --> mp["Map square"] mp --> tk["Take 3"] tk --> col["slices.Collect"] col -. "yield returns false after the third value" .-> tk tk -. "false propagates back" .-> mp mp -. "false" .-> flt flt -. "false" .-> src

Solid arrows show the direction of the values, dashed arrows the direction of the false result: values are pushed downstream, the stop signal travels back upstream through every yield.

Generic types with iterator methods

A generic container conventionally exposes its elements with an All method, mirroring what slices and maps do for the built-in types. The Insert and Collect pair below follows the standard library’s own naming (maps.Insert adds a sequence to a map, maps.Collect builds a map):

package main

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

// Set is a generic set with an iterator method named All.
type Set[V comparable] struct {
	m map[V]struct{}
}

func NewSet[V comparable](vs ...V) *Set[V] {
	s := &Set[V]{m: map[V]struct{}{}}
	for _, v := range vs {
		s.m[v] = struct{}{}
	}
	return s
}

// All returns an iterator over the elements of s in no particular order.
func (s *Set[V]) All() iter.Seq[V] {
	return func(yield func(V) bool) {
		for v := range s.m {
			if !yield(v) {
				return
			}
		}
	}
}

// Insert adds every element of seq to s.
func (s *Set[V]) Insert(seq iter.Seq[V]) {
	for v := range seq {
		s.m[v] = struct{}{}
	}
}

// Collect builds a Set from a sequence.
func Collect[V comparable](seq iter.Seq[V]) *Set[V] {
	s := NewSet[V]()
	s.Insert(seq)
	return s
}

func main() {
	a := NewSet(3, 1, 2)
	b := Collect(slices.Values([]int{2, 3, 4, 3}))
	a.Insert(b.All())
	fmt.Println(slices.Sorted(a.All()))
	fmt.Println(len(a.m))
}
[1 2 3 4]
4

Generic methods (Go 1.27)

Before Go 1.27 a method could not declare type parameters of its own, so a method-style chain seq.Map(f).Filter(g) with a result type different from the receiver’s was impossible: Map had to be a package-level function. Go 1.27 allows a method declaration to introduce its own type parameters (see the generic methods blog post and the Go 1.27 release notes), which lets a collection offer type-changing iterator methods:

package main

import (
	"fmt"
	"iter"
)

type Library struct{ titles []string }

// Generic method (Go 1.27): a method with its own type parameter.
func (l *Library) Map[U any](f func(string) U) iter.Seq[U] {
	return func(yield func(U) bool) {
		for _, t := range l.titles {
			if !yield(f(t)) {
				return
			}
		}
	}
}

func main() {
	l := &Library{titles: []string{"Dune", "Emma"}}
	for n := range l.Map(func(s string) int { return len(s) }) {
		fmt.Println(n)
	}
	for s := range l.Map(func(s string) string { return "<" + s + ">" }) {
		fmt.Println(s)
	}
}
4
4
<Dune>
<Emma>

The restrictions are important. Interface methods cannot declare type parameters, and a generic method cannot satisfy an interface method: a generic method is only reachable through the concrete type. A method-chain API on iter.Seq itself is not possible, because iter.Seq lives in the standard library and methods can only be declared in the defining package; Library above is a type of our own. The package-level adapters of the previous section remain the portable choice and work with any module that can use range-over-func (go 1.23 or newer), whereas generic methods need go 1.27. More in Generics.

Pull iterators: iter.Pull and iter.Pull2

A push iterator calls the caller; a pull iterator is called by the caller. Sometimes the second shape is the one you need, because the consumer must advance two sequences independently (merging, zipping, comparing), or must read one element, do something else, and read the next later. A for loop cannot do that: the iterator owns the loop. iter.Pull converts a push iterator into two functions:

  • next() (V, bool) returns the next value and true, or the zero value and false when the sequence is over;

  • stop() ends the iteration early and lets the iterator clean up.

package main

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

func main() {
	seq := slices.Values([]string{"Dune", "Emma", "Ulysses"})
	next, stop := iter.Pull(seq)
	defer stop() // always call stop, even if the sequence may be exhausted

	for {
		v, ok := next()
		if !ok {
			break
		}
		fmt.Println("pulled", v)
	}
	// After exhaustion (or after stop) next keeps returning the zero value.
	v, ok := next()
	fmt.Printf("after the end: %q %t\n", v, ok)

	// Pull2 for a Seq2.
	next2, stop2 := iter.Pull2(slices.All([]string{"a", "b"}))
	i, s, ok := next2()
	fmt.Println(i, s, ok)
	stop2()
	i, s, ok = next2()
	fmt.Printf("after stop: %d %q %t\n", i, s, ok)
}
pulled Dune
pulled Emma
pulled Ulysses
after the end: "" false
0 a true
after stop: 0 "" false

Source: iter: Pull and iter: Pull2

The documented rules (iter.Pull):

  • it is valid to call next after the end or after stop: it keeps returning the zero value and false;

  • stop must be called when the caller stops before next has returned false; it is valid to call it several times and after the sequence has ended, so the idiom is defer stop() right after the call;

  • next and stop must not be called from several goroutines at once;

  • if the iterator panics during next or stop, that function panics with the same value.

The figure compares the two control flows.

Left: a push iterator calls yield for each value and the loop body returns true to continue or false to break. Right: a pull iterator is suspended inside yield and the caller resumes it with next or ends it with stop

Why stop matters: the iterator is suspended inside yield, in the middle of its own loop. Unless stop makes that yield return false, the iterator never reaches its defer statements and whatever it holds (a file, a lock, a database cursor) stays open. A pull iterator that is abandoned without stop also keeps its coroutine alive until the program ends.

package main

import (
	"fmt"
	"iter"
)

func Count(n int) iter.Seq[int] {
	return func(yield func(int) bool) {
		defer fmt.Println("  iterator: deferred cleanup")
		for i := 1; i <= n; i++ {
			if !yield(i) {
				fmt.Println("  iterator: yield returned false")
				return
			}
		}
	}
}

func Fail() iter.Seq[int] {
	return func(yield func(int) bool) {
		yield(1)
		panic("source failed")
	}
}

func main() {
	next, stop := iter.Pull(Count(5))
	v, ok := next()
	fmt.Println(v, ok)
	fmt.Println("calling stop")
	stop() // makes the pending yield return false so the iterator can clean up
	fmt.Println("stop returned")
	stop() // calling stop again is safe
	v, ok = next()
	fmt.Println(v, ok)

	// A panic inside the iterator is propagated to the caller of next.
	func() {
		defer func() { fmt.Println("recovered:", recover()) }()
		next, stop := iter.Pull(Fail())
		defer stop()
		fmt.Println(next())
		fmt.Println(next())
	}()
}
1 true
calling stop
  iterator: yield returned false
  iterator: deferred cleanup
stop returned
0 false
1 true
recovered: source failed

Source: iter: Pull

The first part shows stop() making the pending yield return false, the iterator printing its message and the deferred cleanup running before stop returns; a second stop and a later next are harmless. The second part shows the panic rule: a panic in the iterator surfaces in the caller’s call to next, where it can be recovered.

Pull iterators are implemented with runtime coroutines: the iterator runs on its own stack and control is handed back and forth directly, without the channel traffic of a hand-written producer goroutine. A next call is still more expensive than an iteration of a plain for loop. Reach for iter.Pull only when a for loop genuinely cannot express the control flow.

Merging and comparing sequences

The two classic uses, merging two sorted sequences and comparing two sequences element by element, need two cursors at once:

package main

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

// Merge combines two sorted sequences into one sorted sequence. It needs to
// advance both inputs independently, so it uses pull iterators.
func Merge[V any](a, b iter.Seq[V], cmp func(V, V) int) iter.Seq[V] {
	return func(yield func(V) bool) {
		nextA, stopA := iter.Pull(a)
		defer stopA()
		nextB, stopB := iter.Pull(b)
		defer stopB()

		va, okA := nextA()
		vb, okB := nextB()
		for okA && okB {
			if cmp(va, vb) <= 0 {
				if !yield(va) {
					return
				}
				va, okA = nextA()
			} else {
				if !yield(vb) {
					return
				}
				vb, okB = nextB()
			}
		}
		for ; okA; va, okA = nextA() {
			if !yield(va) {
				return
			}
		}
		for ; okB; vb, okB = nextB() {
			if !yield(vb) {
				return
			}
		}
	}
}

// Equal reports whether two sequences have the same elements.
func Equal[V comparable](a, b iter.Seq[V]) bool {
	nextA, stopA := iter.Pull(a)
	defer stopA()
	nextB, stopB := iter.Pull(b)
	defer stopB()
	for {
		va, okA := nextA()
		vb, okB := nextB()
		if okA != okB {
			return false
		}
		if !okA {
			return true
		}
		if va != vb {
			return false
		}
	}
}

func main() {
	a := slices.Values([]int{1, 4, 9})
	b := slices.Values([]int{2, 3, 10, 11})
	cmp := func(x, y int) int { return x - y }
	fmt.Println(slices.Collect(Merge(a, b, cmp)))
	fmt.Println(Equal(a, slices.Values([]int{1, 4, 9})), Equal(a, b))
}
[1 2 3 4 9 10 11]
true false

Source: iter: Pull and slices

Merge itself returns a push iterator (iter.Seq), so it composes with slices.Collect, range and the adapters above; the pull iterators are an implementation detail kept inside it, closed by defer stopA() and defer stopB() whether the consumer stops early or reads everything.

Iterators and goroutines

An iterator may use goroutines and channels internally, but yield is always called from the goroutine that runs the iterator, so the loop body never runs concurrently with itself. The loop body is not run on the producer goroutine unless the iterator itself is called from there. The example drains the channel when the body breaks, so the producer goroutine is not leaked (sync.WaitGroup.Go is Go 1.25).

package main

import (
	"fmt"
	"iter"
	"slices"
	"sync"
)

func main() {
	// An iterator may use goroutines and channels internally. yield is still
	// called from the goroutine that runs the iterator, never from the
	// producer, so the loop body never runs concurrently with itself.
	seq := func(yield func(int) bool) {
		var wg sync.WaitGroup
		results := make(chan int)
		wg.Go(func() {
			defer close(results)
			for i := 1; i <= 3; i++ {
				results <- i * i
			}
		})
		for v := range results {
			if !yield(v) {
				// Drain so the producer goroutine is not leaked.
				for range results {
				}
				break
			}
		}
		wg.Wait()
	}
	fmt.Println(slices.Collect(iter.Seq[int](seq)))
	for v := range seq {
		fmt.Println("got", v)
		break
	}
}
[1 4 9]
got 1

An iterator that receives from a channel needs a way out other than the channel closing. Passing a context.Context is the usual answer; the example below also shows the subtlety that select chooses randomly among ready cases, so the context is checked explicitly before each receive to make the behaviour deterministic:

package main

import (
	"context"
	"fmt"
	"iter"
)

// Recv yields values received from ch until ch is closed, ctx is cancelled
// or the loop body breaks.
func Recv[V any](ctx context.Context, ch <-chan V) iter.Seq[V] {
	return func(yield func(V) bool) {
		for {
			if ctx.Err() != nil { // check first: select picks randomly among ready cases
				return
			}
			select {
			case <-ctx.Done():
				return
			case v, ok := <-ch:
				if !ok || !yield(v) {
					return
				}
			}
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	ch := make(chan string, 3)
	ch <- "Dune"
	ch <- "Emma"
	ch <- "Ulysses"
	for title := range Recv(ctx, ch) {
		fmt.Println("received", title)
		if title == "Emma" {
			cancel() // the next loop iteration observes the cancellation
		}
	}
	fmt.Println("ctx error:", ctx.Err())
}
received Dune
received Emma
ctx error: context canceled

See Channels and Select, Context and Concurrency Patterns for the underlying concepts.

Iterator helpers in the standard library

Since Go 1.23 many packages return iterators or accept them. The tables list every function present in Go 1.27 (checked with go doc on go1.27.2).

slices (Go 1.23)

Function Returns / does Notes

slices.All(s)

iter.Seq2[int, E] index and value

same pairs as range s

slices.Values(s)

iter.Seq[E]

the usual way to turn a slice into a Seq

slices.Backward(s)

iter.Seq2[int, E] in descending index order

no more for i := len(s)-1; i >= 0; i--

slices.Collect(seq)

[]E from a sequence

returns nil for an empty sequence

slices.AppendSeq(s, seq)

appends a sequence to s

reuses capacity of s

slices.Sorted(seq)

collects then sorts

element type must be cmp.Ordered

slices.SortedFunc(seq, cmp)

collects then sorts with a comparison function

not stable

slices.SortedStableFunc(seq, cmp)

the stable variant

keeps the sequence order of equal elements

slices.Chunk(s, n)

iter.Seq[[]E] of consecutive sub-slices of length at most n

sub-slices are clipped (capacity equals length); panics if n < 1

maps (Go 1.23)

Function Returns / does

maps.All(m)

iter.Seq2[K, V] of all pairs, in the (random) order of range m

maps.Keys(m)

iter.Seq[K]

maps.Values(m)

iter.Seq[V]

maps.Insert(m, seq)

adds the pairs of a Seq2; existing keys are overwritten

maps.Collect(seq)

builds a new map from a Seq2

strings and bytes (Go 1.24)

Function Returns / does

strings.Lines(s)

each line, including its terminating newline; no final empty line; single-use

strings.SplitSeq(s, sep)

substrings around sep, like strings.Split without the slice

strings.SplitAfterSeq(s, sep)

substrings, each keeping its trailing sep

strings.FieldsSeq(s)

fields separated by runs of white space, like strings.Fields

strings.FieldsFuncSeq(s, f)

fields separated by runs of runes satisfying f

bytes.Lines, bytes.SplitSeq, bytes.SplitAfterSeq, bytes.FieldsSeq, bytes.FieldsFuncSeq

the same, yielding []byte sub-slices that alias the input

package main

import (
	"fmt"
	"maps"
	"slices"
	"strings"
)

type Book struct {
	Title string
	Year  int
}

func main() {
	books := []Book{{"Dune", 1965}, {"Emma", 1815}, {"Ulysses", 1922}, {"Hamlet", 1603}, {"Dracula", 1897}}

	// All and Values: index/value and value-only iterators.
	for i, b := range slices.All(books[:2]) {
		fmt.Println("All:", i, b.Title)
	}
	fmt.Println("Values:", slices.Collect(slices.Values([]int{3, 1, 2})))

	// Backward: reverse iteration without writing an index loop.
	for i, b := range slices.Backward(books[:3]) {
		fmt.Println("Backward:", i, b.Title)
	}

	// Collect and Sorted turn a sequence into a slice.
	years := slices.Collect(func(yield func(int) bool) {
		for _, b := range books {
			if !yield(b.Year) {
				return
			}
		}
	})
	fmt.Println("Collect:", years)
	fmt.Println("Sorted:", slices.Sorted(slices.Values(years)))
	byTitle := slices.SortedFunc(slices.Values(books), func(a, b Book) int {
		return strings.Compare(a.Title, b.Title)
	})
	fmt.Println("SortedFunc:", byTitle[0].Title, byTitle[len(byTitle)-1].Title)

	// Chunk splits a slice into sub-slices of at most n elements.
	for c := range slices.Chunk([]int{1, 2, 3, 4, 5}, 2) {
		fmt.Println("Chunk:", c, len(c), cap(c))
	}

	// AppendSeq appends a sequence to an existing slice.
	fmt.Println("AppendSeq:", slices.AppendSeq([]int{0}, slices.Values([]int{7, 8})))

	// maps: iteration order of a map is random, so sort before printing.
	stock := map[string]int{"Dune": 3, "Emma": 0, "Ulysses": 1}
	fmt.Println("Keys:", slices.Sorted(maps.Keys(stock)))
	fmt.Println("Values:", slices.Sorted(maps.Values(stock)))
	clone := maps.Collect(maps.All(stock))
	maps.Insert(clone, maps.All(map[string]int{"Hamlet": 2}))
	fmt.Println("Collect+Insert:", len(clone), maps.Equal(clone, stock))
}
All: 0 Dune
All: 1 Emma
Values: [3 1 2]
Backward: 2 Ulysses
Backward: 1 Emma
Backward: 0 Dune
Collect: [1965 1815 1922 1603 1897]
Sorted: [1603 1815 1897 1922 1965]
SortedFunc: Dracula Ulysses
Chunk: [1 2] 2 2
Chunk: [3 4] 2 2
Chunk: [5] 1 1
AppendSeq: [0 7 8]
Keys: [Dune Emma Ulysses]
Values: [0 1 3]
Collect+Insert: 4 false

Source: slices and maps

The order of maps.Keys and maps.Values is the random order of a map range (see Maps), so deterministic output needs slices.Sorted(maps.Keys(m)), the Go 1.23 replacement for the old "collect the keys, sort them, loop" routine. slices.Collect(maps.Keys(m)) is the one-liner that replaced the pre-1.21 key-collecting loop and the golang.org/x/exp/maps helpers that returned slices.

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	text := "Dune;Emma\nUlysses;Hamlet\n"

	for line := range strings.Lines(text) {
		fmt.Printf("Lines: %q\n", line)
	}
	for part := range strings.SplitSeq("Dune;Emma;Ulysses", ";") {
		fmt.Println("SplitSeq:", part)
	}
	for part := range strings.SplitAfterSeq("a,b,c", ",") {
		fmt.Printf("SplitAfterSeq: %q\n", part)
	}
	for f := range strings.FieldsSeq("  the  left hand\tof darkness ") {
		fmt.Println("FieldsSeq:", f)
	}
	for f := range strings.FieldsFuncSeq("a1b22c", func(r rune) bool { return r >= '0' && r <= '9' }) {
		fmt.Println("FieldsFuncSeq:", f)
	}
	// The bytes package mirrors them and yields sub-slices of the input.
	for f := range bytes.FieldsSeq([]byte("x y")) {
		fmt.Printf("bytes.FieldsSeq: %s\n", f)
	}
}
Lines: "Dune;Emma\n"
Lines: "Ulysses;Hamlet\n"
SplitSeq: Dune
SplitSeq: Emma
SplitSeq: Ulysses
SplitAfterSeq: "a,"
SplitAfterSeq: "b,"
SplitAfterSeq: "c"
FieldsSeq: the
FieldsSeq: left
FieldsSeq: hand
FieldsSeq: of
FieldsSeq: darkness
FieldsFuncSeq: a
FieldsFuncSeq: b
FieldsFuncSeq: c
bytes.FieldsSeq: x
bytes.FieldsSeq: y

Source: strings and bytes

The Seq variants avoid building the intermediate slice that strings.Split allocates, and Lines avoids both the slice and a bufio.Scanner when the text is already in memory. Use strings.Split when you need the slice anyway.

reflect (Go 1.23 and Go 1.26)

Function Returns / does

Value.Seq(), Value.Seq2() (Go 1.23)

iterate over a value the way range would: slice, array, string, map, channel, integer, pointer to array or a range-over-func function value

Type.CanSeq(), Type.CanSeq2() (Go 1.23)

report whether the type may be iterated with Seq and Seq2

Type.Fields(), Type.Methods(), Type.Ins(), Type.Outs() (Go 1.26)

iterators over the struct fields, methods, input and output parameters of a type

Value.Fields(), Value.Methods() (Go 1.26)

iterators over a value’s fields or methods together with their values

package main

import (
	"fmt"
	"reflect"
)

type Book struct {
	Title string `json:"title"`
	Pages int    `json:"pages"`
}

func (b Book) Summary() string { return fmt.Sprintf("%s (%d pages)", b.Title, b.Pages) }
func (b *Book) Grow(n int)     { b.Pages += n }

func lookup(a int, b string) (bool, error) { return false, nil }

func main() {
	t := reflect.TypeFor[Book]()
	for f := range t.Fields() {
		fmt.Println("field:", f.Name, f.Type, f.Tag.Get("json"))
	}
	for m := range reflect.TypeFor[*Book]().Methods() {
		fmt.Println("method:", m.Name)
	}
	ft := reflect.TypeOf(lookup)
	for in := range ft.Ins() {
		fmt.Println("in:", in)
	}
	for out := range ft.Outs() {
		fmt.Println("out:", out)
	}

	// Value.Fields yields the field description and the field value together.
	b := Book{"Dune", 412}
	for f, v := range reflect.ValueOf(b).Fields() {
		fmt.Println("value:", f.Name, v)
	}
	// Value.Seq ranges over anything a range statement accepts.
	for v := range reflect.ValueOf([]string{"a", "b"}).Seq() {
		fmt.Println("Seq:", v)
	}
	for k, v := range reflect.ValueOf(map[string]int{"x": 1}).Seq2() {
		fmt.Println("Seq2:", k, v)
	}
	for v := range reflect.ValueOf(3).Seq() {
		fmt.Print(v, " ")
	}
	fmt.Println()
	fmt.Println(reflect.TypeFor[[]int]().CanSeq(), reflect.TypeFor[string]().CanSeq2())
}
field: Title string title
field: Pages int pages
method: Grow
method: Summary
in: int
in: string
out: bool
out: error
value: Title Dune
value: Pages 412
Seq: 0
Seq: 1
Seq2: x 1
0 1 2
true true

See Reflection for the rest of the package.

Other packages

  • go/ast.Preorder(root) (Go 1.23) yields every node of a syntax tree in depth-first order, replacing most ast.Inspect closures. go/types (Go 1.24) added to its list-like types, those with a Len and an At method, methods that return iterators (for example Params().Variables()), replacing index loops over Len and At.

  • text/template (Go 1.24) supports range over integers and over function iterators, so a template can range over an iter.Seq value.

  • sync.WaitGroup.Go (Go 1.25) and the other concurrency helpers are not iterators but are the natural companion of the producer-goroutine patterns above.

  • Go 1.28, expected in February 2027, has not been released at the time of writing and its release notes are still a stub. The regexp and flag packages have no iterator functions in Go 1.27 (checked with go doc); iterator methods for them are expected in Go 1.28 and should be confirmed in the final release notes (Go 1.28 release notes) before being relied on.

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
)

func main() {
	src := `package catalog

type Book struct{ Title string }

func (b Book) Summary() string { return b.Title }
func Lend(b Book) error        { return nil }
`
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, "catalog.go", src, 0)
	if err != nil {
		fmt.Println(err)
		return
	}
	// ast.Preorder (Go 1.23) replaces ast.Inspect with a closure.
	for n := range ast.Preorder(f) {
		if fn, ok := n.(*ast.FuncDecl); ok {
			fmt.Println("func", fn.Name.Name, "at line", fset.Position(fn.Pos()).Line)
		}
	}
}
func Summary at line 5
func Lend at line 6

Naming conventions

The iter package documentation (Naming Conventions) records the conventions that the standard library follows and that user code should copy:

Situation Convention

a method that walks all the elements of a collection

name it All; it returns iter.Seq[V] (or iter.Seq2[K, V] for pairs)

a function that returns an iterator over part of a collection

name it after the sequence it walks: Keys, Values, Backward, ByAuthor, Lines

a function producing an iterator from a slice-like argument

Values(s) for single values, All(s) for index-value pairs

a function that builds a collection from an iterator

Collect, Sorted, AppendSeq, Insert

an iterator over a collection with several natural sequences

one method per sequence (Keys, Values), plus All for the pairs

an iterator that cannot be restarted

say so in its documentation (single-use iterator)

an iterator that may fail

iter.Seq2[V, error] or an Err method, documented

Other guidance, from the standard library’s own use and the blog post: return iter.Seq rather than a concrete function type; take an iter.Seq parameter instead of a slice when the function only needs to read the elements once; keep one Seq per kind of walk instead of adding flags; avoid an iterator that needs a Close method (use defer inside the iterator or a pull iterator with stop); and do not return an iterator from a function that already allocates the whole slice, because returning the slice is simpler and often faster.

Testing iterators

Testing an iterator means checking its values and its behaviour when the loop leaves early: break, return and the requirement that producers stop. The test below checks laziness (the producer ran exactly as many times as needed) and measures allocations of a plain range over slices.Values with testing.AllocsPerRun. The go.mod is the one above; the package and test files are seq.go and seq_test.go.

// Package seq holds small iterator adapters.
package seq

import "iter"

// Take yields at most n elements of s.
func Take[V any](s iter.Seq[V], n int) iter.Seq[V] {
	return func(yield func(V) bool) {
		if n <= 0 {
			return
		}
		i := 0
		for v := range s {
			if !yield(v) {
				return
			}
			i++
			if i == n {
				return
			}
		}
	}
}

// Counter yields 0, 1, 2, ... and records how many values it produced.
func Counter(produced *int) iter.Seq[int] {
	return func(yield func(int) bool) {
		for i := 0; ; i++ {
			*produced++
			if !yield(i) {
				return
			}
		}
	}
}

Source: iter: Seq

package seq

import (
	"slices"
	"testing"
)

func TestTake(t *testing.T) {
	var produced int
	got := slices.Collect(Take(Counter(&produced), 3))
	if want := []int{0, 1, 2}; !slices.Equal(got, want) {
		t.Fatalf("got %v, want %v", got, want)
	}
	// Take must stop the producer as soon as it has enough elements.
	if produced != 3 {
		t.Errorf("producer ran %d times, want 3", produced)
	}
}

func TestTakeZero(t *testing.T) {
	var produced int
	for range Take(Counter(&produced), 0) {
		t.Fatal("body must not run")
	}
	if produced != 0 {
		t.Errorf("producer ran %d times, want 0", produced)
	}
}

func TestSliceIterationDoesNotAllocate(t *testing.T) {
	data := []int{1, 2, 3, 4}
	allocs := testing.AllocsPerRun(100, func() {
		sum := 0
		for v := range slices.Values(data) {
			sum += v
		}
		_ = sum
	})
	t.Logf("allocations per run: %v", allocs)
}
$ go test -v -count=1 .
=== RUN   TestTake
--- PASS: TestTake (0.00s)
=== RUN   TestTakeZero
--- PASS: TestTakeZero (0.00s)
=== RUN   TestSliceIterationDoesNotAllocate
    seq_test.go:39: allocations per run: 0
--- PASS: TestSliceIterationDoesNotAllocate (0.00s)
PASS
ok  	example.com/shelf/t01	0.061s

The allocation count is zero here, which is what inlining the small iterator and the loop body makes possible. That is an optimisation, not a language guarantee: closures that escape or iterators that are too large to inline may allocate, so measure with a benchmark (b.Loop, see Benchmarks, Fuzzing and synctest) before replacing a hot index loop. As a rule of thumb, ranging over a function costs a few indirect calls per element, which is irrelevant next to I/O and usually small next to the work done per element, but it is not free.

See Testing for the testing package.

Compile errors and vet reports

Range over function has a small set of compile errors, all copied here from real runs of go build with Go 1.27.2.

Too many iteration variables. The function has a one-argument yield, so only one variable is allowed:

package main

import "fmt"

func Count(n int) func(yield func(int) bool) {
	return func(yield func(int) bool) {
		for i := range n {
			if !yield(i) {
				return
			}
		}
	}
}

func main() {
	for i, v := range Count(3) {
		fmt.Println(i, v)
	}
}
# example.com/shelf/k01
k01/main.go:16:9: range over Count(3) (value of type func(yield func(int) bool)) permits only one iteration variable

Wrong shape. yield must return bool, have at most two parameters, and the iterator must have no results:

package main

import "fmt"

func NoBool(yield func(int)) {
	yield(1)
}

func Extra(yield func(int, int, int) bool) {
	yield(1, 2, 3)
}

func Result(yield func(int) bool) bool {
	return yield(1)
}

func main() {
	for v := range NoBool {
		fmt.Println(v)
	}
	for v := range Extra {
		fmt.Println(v)
	}
	for v := range Result {
		fmt.Println(v)
	}
}
# example.com/shelf/k02
k02/main.go:18:17: cannot range over NoBool (value of type func(yield func(int))): func must be func(yield func(...) bool): yield func does not return bool
k02/main.go:21:17: cannot range over Extra (value of type func(yield func(int, int, int) bool)): func must be func(yield func(...) bool): yield func has too many parameters
k02/main.go:24:17: cannot range over Result (value of type func(yield func(int) bool) bool): func must be func(yield func(...) bool): unexpected results

No iteration variables allowed. func(yield func() bool) has nothing to assign:

package main

import "fmt"

func Ticks(yield func() bool) {
	yield()
}

func main() {
	for i := range Ticks {
		fmt.Println(i)
	}
	var f func(func(int) bool)
	for range f {
	}
}
# example.com/shelf/k03
k03/main.go:10:6: range over Ticks (value of type func(yield func() bool)) permits no iteration variables

Language version too old. The same program as the first example, in a module whose go.mod says go 1.22:

module example.com/old

go 1.22
# example.com/old/e01
e01/main.go:39:17: cannot range over Count(3) (value of type func(yield func(int) bool)): requires go1.23 or later (-lang was set to go1.22; check go.mod)
e01/main.go:42:20: cannot range over Pairs([]string{…}) (value of type func(yield func(int, string) bool)): requires go1.23 or later (-lang was set to go1.22; check go.mod)
e01/main.go:45:12: cannot range over Ticks(2) (value of type func(yield func() bool)): requires go1.23 or later (-lang was set to go1.22; check go.mod)

go vet has no analyzer that checks the yield contract in Go 1.27. The existing analyzers still apply inside iterators and loop bodies (copylocks, lostcancel), and go fix carries modernizers that move older code to iterators (see Formatting, vet, fix and Static Analysis): stringsseq rewrites a range over strings.Split or strings.Fields into SplitSeq or FieldsSeq, slicesbackward rewrites a reversed index loop into slices.Backward, mapsloop replaces explicit loops over maps with calls to the maps package, and stditerators replaces Len/At-style loops with the iterator methods the standard library now offers.

Pitfalls

  • Ignoring the result of yield. The iterator keeps running after break: a run-time panic at best, a resource leak or a repeated side effect at worst. Always write if !yield(v) { return }.

  • Returning without propagating false through nested loops. In an adapter, the return inside the inner for ... range must also stop the outer loop; a break that only leaves the inner loop makes the outer iterator call yield again.

  • Forgetting defer stop() after iter.Pull: the iterator is left suspended and its cleanup never runs.

  • Single-use iterators ranged twice. The second loop sees nothing (or an error) and no compiler diagnostic exists.

  • Calling yield concurrently or from a goroutine other than the one running the iterator, is a data race on the loop body’s variables, and a panic if the loop has already ended.

  • Capturing the loop variable. Variables of the loop header are per iteration since Go 1.22, so closures and goroutines started inside the body see their own copy; only a module with an older go line still has the shared-variable problem, and such a module cannot use range-over-func anyway.

  • Assuming order. maps.Keys, maps.Values and maps.All follow the random order of a map range. Sort with slices.Sorted when the result is displayed or compared.

  • Expecting a snapshot. slices.Values(s) and slices.All(s) read the slice lazily. Appending to s while the loop is running does not extend the loop (the slice header was captured), but writing to its elements is visible to later iterations, and maps.Keys(m) of a map being modified follows the usual rules for mutating a map during range.

  • Boxing with any. An iter.Seq[any] hides the element type; prefer a concrete or generic type.

  • Deep recursion. A recursive iterator over a very deep structure still uses stack depth proportional to the depth of the structure.

Version notes

Release Change

Go 1.18

type parameters, the prerequisite for generic iter.Seq[V]

Go 1.21

slices, maps and cmp enter the standard library (the non-iterator functions)

Go 1.22

range over integers and per-iteration loop variables; the GOEXPERIMENT=rangefunc preview existed behind a flag in 1.22 only

Go 1.23

range over function types, the iter package with Seq, Seq2, Pull, Pull2; slices.All, Values, Backward, Collect, AppendSeq, Sorted, SortedFunc, SortedStableFunc, Chunk; maps.All, Keys, Values, Insert, Collect; reflect.Value.Seq/Seq2; go/ast.Preorder

Go 1.24

strings and bytes iterators (Lines, SplitSeq, SplitAfterSeq, FieldsSeq, FieldsFuncSeq); iterator methods in go/types; text/template ranges over functions and integers

Go 1.25

sync.WaitGroup.Go, handy for iterator producers

Go 1.26

reflect.Type.Fields, Methods, Ins, Outs and reflect.Value.Fields, Methods

Go 1.27

generic methods (methods with their own type parameters)

Go 1.28 (expected)

iterator methods for regexp and flag are expected; not available in Go 1.27

Go 1.23 is the oldest release that supports the feature. Releases older than the two currently supported ones are out of support, so a module can normally rely on range-over-func; the go line of go.mod still gates it, and Toolchain Versions and Compatibility explains how that line is chosen.

Where the books are outdated

  • Bodner, ch. 4 (control flow) lists the for forms and the types that for-range accepts: only built-in compound types, strings and channels. Since Go 1.22 integers are accepted and since Go 1.23 functions. His advice to copy the loop variable with v := v before capturing it in a closure or goroutine is not needed in a module with go 1.22 or newer.

  • Bodner, ch. 15 (generics) builds generic containers such as a tree and notes that user-defined types cannot be ranged over, and lists method type parameters among the things generics leave out; both limits are gone (iterators in Go 1.23, generic methods in Go 1.27). Range over functions and iter.Seq make a container’s All method the standard answer, and slices/maps give the common helpers.

  • Bodner, ch. 10 (concurrency) presents channels and goroutines as the building block for pipelines. For simple generators and filters, iterators avoid the goroutine and the synchronisation; channels remain the right tool when the stages are truly concurrent (see Concurrency Patterns).

  • Vivien, ch. 3 (control flow) and ch. 7 (data structures) describe the for range forms available at the time (a Go 1.6 era book) and show arrays, slices, strings, maps and channels as the only things that can be ranged over; getting sorted map keys requires a manual slice and a sort call, now slices.Sorted(maps.Keys(m)). Vivien ch. 9 builds generators from goroutines and channels; for plain sequences an iterator needs neither.

  • Neither book mentions iter, iter.Pull, the iterator helpers of slices, maps, strings and bytes, strings.Lines or the reflect iterators.