Skip to content
AgileLogics
7 min read

Iterators: range-over-func

Prerequisites: Advanced generics

Go 1.23 lets for ... range loop over a function. That gives every collection — standard or your own — one uniform way to be iterated, without building an intermediate slice.

The shape of an iterator

An iterator is a function that takes a yield callback and calls it once per element. The iter package names the two common shapes:

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

yield returns false when the loop body wants to stop (a break, return, or goto out of the loop). The iterator must stop calling yield at that point.

package main

import (
    "fmt"
    "iter"
)

func Countdown(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := n; i > 0; i-- {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    for v := range Countdown(5) {
        if v == 2 {
            break
        }
        fmt.Println(v)
    }
}

Expected output:

5
4
3

The break makes yield return false, and Countdown returns early. If an iterator ignores that and calls yield again, the program panics at runtime.

Iterators in the standard library

slices and maps gained iterator functions in Go 1.23:

package main

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

func main() {
    langs := []string{"go", "rust", "zig"}

    for i, v := range slices.All(langs) { // iter.Seq2[int, string]
        fmt.Println(i, v)
    }

    total := 0
    for v := range slices.Values(langs) { // iter.Seq[string]
        total += len(v)
    }
    fmt.Println("total length:", total)

    ages := map[string]int{"ada": 36, "alan": 41, "grace": 85}
    // Map iteration order is random; sort the keys for stable output.
    for _, name := range slices.Sorted(maps.Keys(ages)) {
        fmt.Println(name, ages[name])
    }

    vals := slices.Collect(maps.Values(map[string]int{"x": 1}))
    fmt.Println(vals)
}

Expected output:

0 go
1 rust
2 zig
total length: 9
ada 36
alan 41
grace 85
[1]

slices.Collect turns an iter.Seq into a slice; slices.Sorted collects and sorts in one step; maps.Collect builds a map from an iter.Seq2.

Adding iteration to your own types

Return an iterator from a method. This keeps the internal representation private while still allowing range:

func (s *Stack[T]) All() iter.Seq[T] {
    return func(yield func(T) bool) {
        for i := len(s.items) - 1; i >= 0; i-- {
            if !yield(s.items[i]) {
                return
            }
        }
    }
}

// for v := range stack.All() { ... }

Composing iterators

Because iterators are values, you can write adapters that take one iterator and return another — filtering, mapping, or limiting it. Nothing runs until something ranges over the result, and elements are processed one at a time with no intermediate slice. The practice exercise below builds one.

Pull iterators

iter.Pull converts a push iterator into a next() function, for the rarer case where you need to step through two sequences in lockstep. Always call the returned stop function (usually with defer).

Common mistakes

  • Ignoring yield's return value — the loop body's break then triggers a runtime panic.
  • Returning an iterator over a map and expecting a stable order. Map order is deliberately randomised; sort the keys if order matters.
  • Converting to a slice just to loop once. for v := range seq is enough.
  • Mutating the underlying collection while an iterator over it is running.

Practice

Write a generic adapter Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V], then use it with slices.Values to print the even numbers from []int{1, 2, 3, 4, 5, 6}.

Show solution
package main

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

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 main() {
    nums := []int{1, 2, 3, 4, 5, 6}
    evens := Filter(slices.Values(nums), func(n int) bool { return n%2 == 0 })
    for n := range evens {
        fmt.Println(n)
    }
}

Expected output:

2
4
6