Go by Example

Range over Iterators

Go 1.23 allows range to iterate over functions - enabling lazy, composable iterators without allocating intermediate slices.

Go 1.23 extended the range keyword to accept functions. An iterator function calls a yield callback for each value; when yield returns false, the iterator must stop. This enables lazy, composable iteration pipelines without allocating intermediate slices.

An iter.Seq[V] is a function with signature func(yield func(V) bool). You write the iteration logic and call yield for each element. The caller uses it with an ordinary for ... range loop.

package main
 
import (
    "fmt"
    "iter"
)
 
// Fibonacci returns a lazy iterator of Fibonacci numbers up to max
func Fibonacci(max int) iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for a <= max {
            if !yield(a) {
                return // caller broke out of the loop
            }
            a, b = b, a+b
        }
    }
}
 
func main() {
    for n := range Fibonacci(100) {
        fmt.Print(n, " ")
    }
    // 0 1 1 2 3 5 8 13 21 34 55 89
    fmt.Println()
}

iter.Seq2[K, V] yields two values per iteration - the same as ranging over a map or slice with both index and value. Use it when the position or key is meaningful alongside the value.

package main
 
import (
    "fmt"
    "iter"
    "strings"
)
 
// Lines yields (lineNumber, text) pairs from a multi-line string
func Lines(s string) iter.Seq2[int, string] {
    return func(yield func(int, string) bool) {
        for i, line := range strings.Split(s, "\n") {
            if !yield(i+1, line) {
                return
            }
        }
    }
}
 
func main() {
    src := "first\nsecond\nthird"
    for n, line := range Lines(src) {
        fmt.Printf("%d: %s\n", n, line)
    }
}

Iterators compose well. A Filter adapter wraps any iter.Seq[V] and yields only the elements that satisfy a predicate - the underlying iteration is lazy and no intermediate slice is ever allocated.

package main
 
import (
    "fmt"
    "iter"
)
 
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) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}
 
func Integers(start, end int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; i < end; i++ {
            if !yield(i) {
                return
            }
        }
    }
}
 
func main() {
    evens := Filter(Integers(0, 10), func(n int) bool { return n%2 == 0 })
    for n := range evens {
        fmt.Print(n, " ") // 0 2 4 6 8
    }
    fmt.Println()
}

In production

Range-over-func enables the filter-map-take pipeline pattern without loading entire datasets into memory - particularly valuable when processing large files, database cursor results, or streaming API responses. The key invariant: your iterator must respect yield returning false and stop immediately, otherwise a break in the caller's loop will silently not work. This feature requires Go 1.23 or later - check your team's minimum version before adopting it. The iter package in the standard library provides the canonical type aliases (iter.Seq, iter.Seq2) and the iter.Pull adapter for converting push iterators to pull iterators when you need to interleave two sequences.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free