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'sbreakthen 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 seqis 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