Quidest?

Go range-over-func

Range Over Function (Custom Iterators) in Go

Range-over-func was introduced in Go 1.23 (released August 2024) and allows you to use the range keyword with custom iterator functions, making it possible to create your own iterable types.

Core Concepts

The Iterator Function Signature

Iterator functions have specific signatures defined in the iter package:

1// Iterator that yields single values
2type Seq[V any] func(yield func(V) bool)
3
4// Iterator that yields key-value pairs
5type Seq2[K, V any] func(yield func(K, V) bool)

Basic Usage

 1import "iter"
 2
 3// Simple iterator that yields numbers 1 to n
 4func Count(n int) iter.Seq[int] {
 5    return func(yield func(int) bool) {
 6        for i := 1; i <= n; i++ {
 7            if !yield(i) {
 8                return // Consumer stopped iterating
 9            }
10        }
11    }
12}
13
14// Use it with range
15for num := range Count(5) {
16    fmt.Println(num) // Prints 1, 2, 3, 4, 5
17}

How It Works

The yield function is called by the iterator to produce values:

 1func Fibonacci(max int) iter.Seq[int] {
 2    return func(yield func(int) bool) {
 3        a, b := 0, 1
 4        for a <= max {
 5            if !yield(a) {
 6                return // Stop if consumer breaks
 7            }
 8            a, b = b, a+b
 9        }
10    }
11}
12
13for num := range Fibonacci(100) {
14    fmt.Println(num)
15    if num > 50 {
16        break // yield returns false, iterator stops
17    }
18}

Seq2: Key-Value Iterators

For iterating pairs of values (like map entries):

 1// Iterator that yields index and value
 2func Enumerate[V any](slice []V) iter.Seq2[int, V] {
 3    return func(yield func(int, V) bool) {
 4        for i, v := range slice {
 5            if !yield(i, v) {
 6                return
 7            }
 8        }
 9    }
10}
11
12// Usage
13fruits := []string{"apple", "banana", "cherry"}
14for idx, fruit := range Enumerate(fruits) {
15    fmt.Printf("%d: %s\n", idx, fruit)
16}

Creating Custom Iterators

Tree Traversal

 1type Tree[T any] struct {
 2    Value T
 3    Left  *Tree[T]
 4    Right *Tree[T]
 5}
 6
 7// In-order traversal
 8func (t *Tree[T]) InOrder() iter.Seq[T] {
 9    return func(yield func(T) bool) {
10        t.inOrder(yield)
11    }
12}
13
14func (t *Tree[T]) inOrder(yield func(T) bool) bool {
15    if t == nil {
16        return true
17    }
18    if !t.Left.inOrder(yield) {
19        return false
20    }
21    if !yield(t.Value) {
22        return false
23    }
24    return t.Right.inOrder(yield)
25}
26
27// Usage
28tree := &Tree[int]{
29    Value: 5,
30    Left:  &Tree[int]{Value: 3},
31    Right: &Tree[int]{Value: 7},
32}
33
34for value := range tree.InOrder() {
35    fmt.Println(value) // 3, 5, 7
36}

File Line Iterator

 1func Lines(filename string) iter.Seq[string] {
 2    return func(yield func(string) bool) {
 3        file, err := os.Open(filename)
 4        if err != nil {
 5            return
 6        }
 7        defer file.Close()
 8
 9        scanner := bufio.NewScanner(file)
10        for scanner.Scan() {
11            if !yield(scanner.Text()) {
12                return
13            }
14        }
15    }
16}
17
18// Usage
19for line := range Lines("data.txt") {
20    fmt.Println(line)
21}

Infinite Iterators

 1func Repeat[T any](value T) iter.Seq[T] {
 2    return func(yield func(T) bool) {
 3        for {
 4            if !yield(value) {
 5                return
 6            }
 7        }
 8    }
 9}
10
11func Natural() iter.Seq[int] {
12    return func(yield func(int) bool) {
13        for i := 0; ; i++ {
14            if !yield(i) {
15                return
16            }
17        }
18    }
19}
20
21// Usage (must break or it runs forever)
22count := 0
23for n := range Natural() {
24    fmt.Println(n)
25    count++
26    if count >= 10 {
27        break
28    }
29}

Iterator Adapters/Combinators

Filter

 1func Filter[V any](seq iter.Seq[V], predicate func(V) bool) iter.Seq[V] {
 2    return func(yield func(V) bool) {
 3        for v := range seq {
 4            if predicate(v) {
 5                if !yield(v) {
 6                    return
 7                }
 8            }
 9        }
10    }
11}
12
13// Usage
14evens := Filter(Count(10), func(n int) bool {
15    return n%2 == 0
16})
17
18for n := range evens {
19    fmt.Println(n) // 2, 4, 6, 8, 10
20}

Map/Transform

 1func Map[V, U any](seq iter.Seq[V], transform func(V) U) iter.Seq[U] {
 2    return func(yield func(U) bool) {
 3        for v := range seq {
 4            if !yield(transform(v)) {
 5                return
 6            }
 7        }
 8    }
 9}
10
11// Usage
12doubled := Map(Count(5), func(n int) int {
13    return n * 2
14})
15
16for n := range doubled {
17    fmt.Println(n) // 2, 4, 6, 8, 10
18}

Take

 1func Take[V any](seq iter.Seq[V], n int) iter.Seq[V] {
 2    return func(yield func(V) bool) {
 3        count := 0
 4        for v := range seq {
 5            if count >= n {
 6                return
 7            }
 8            if !yield(v) {
 9                return
10            }
11            count++
12        }
13    }
14}
15
16// Usage
17first5 := Take(Natural(), 5)
18for n := range first5 {
19    fmt.Println(n) // 0, 1, 2, 3, 4
20}

Zip

 1func Zip[A, B any](a iter.Seq[A], b iter.Seq[B]) iter.Seq2[A, B] {
 2    return func(yield func(A, B) bool) {
 3        next, stop := iter.Pull(b)
 4        defer stop()
 5
 6        for valA := range a {
 7            valB, ok := next()
 8            if !ok {
 9                return
10            }
11            if !yield(valA, valB) {
12                return
13            }
14        }
15    }
16}
17
18// Usage
19letters := []string{"a", "b", "c"}
20numbers := []int{1, 2, 3}
21
22for letter, num := range Zip(slices.Values(letters), slices.Values(numbers)) {
23    fmt.Printf("%s: %d\n", letter, num)
24}

Chain

 1func Chain[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
 2    return func(yield func(V) bool) {
 3        for _, seq := range seqs {
 4            for v := range seq {
 5                if !yield(v) {
 6                    return
 7                }
 8            }
 9        }
10    }
11}
12
13// Usage
14combined := Chain(Count(3), Count(3))
15for n := range combined {
16    fmt.Println(n) // 1, 2, 3, 1, 2, 3
17}

Pull Iterators

Go 1.23 also introduced iter.Pull and iter.Pull2 for converting push-style iterators to pull-style:

 1// Pull converts a push iterator to a pull iterator
 2next, stop := iter.Pull(Count(5))
 3defer stop() // Important: always call stop to clean up
 4
 5for {
 6    val, ok := next()
 7    if !ok {
 8        break
 9    }
10    fmt.Println(val)
11}

When to Use Pull

Pull iterators are useful when you need:

 1func Merge[T constraints.Ordered](a, b iter.Seq[T]) iter.Seq[T] {
 2    return func(yield func(T) bool) {
 3        nextA, stopA := iter.Pull(a)
 4        defer stopA()
 5        nextB, stopB := iter.Pull(b)
 6        defer stopB()
 7
 8        valA, okA := nextA()
 9        valB, okB := nextB()
10
11        for okA && okB {
12            if valA <= valB {
13                if !yield(valA) {
14                    return
15                }
16                valA, okA = nextA()
17            } else {
18                if !yield(valB) {
19                    return
20                }
21                valB, okB = nextB()
22            }
23        }
24
25        // Drain remaining
26        for okA {
27            if !yield(valA) {
28                return
29            }
30            valA, okA = nextA()
31        }
32        for okB {
33            if !yield(valB) {
34                return
35            }
36            valB, okB = nextB()
37        }
38    }
39}

Standard Library Integration

slices Package

 1import "slices"
 2
 3// Iterate over values
 4for v := range slices.Values([]int{1, 2, 3}) {
 5    fmt.Println(v)
 6}
 7
 8// Iterate over indices and values
 9for i, v := range slices.All([]string{"a", "b", "c"}) {
10    fmt.Printf("%d: %s\n", i, v)
11}
12
13// Backward iteration
14for i, v := range slices.Backward([]int{1, 2, 3}) {
15    fmt.Printf("%d: %d\n", i, v) // 2:3, 1:2, 0:1
16}

maps Package

 1import "maps"
 2
 3m := map[string]int{"a": 1, "b": 2}
 4
 5// Iterate over keys
 6for k := range maps.Keys(m) {
 7    fmt.Println(k)
 8}
 9
10// Iterate over values
11for v := range maps.Values(m) {
12    fmt.Println(v)
13}
14
15// Iterate over key-value pairs
16for k, v := range maps.All(m) {
17    fmt.Printf("%s: %d\n", k, v)
18}

Error Handling

Iterators don’t have built-in error handling. Common patterns:

Pattern 1: Panic on Error

 1func MustLines(filename string) iter.Seq[string] {
 2    return func(yield func(string) bool) {
 3        file, err := os.Open(filename)
 4        if err != nil {
 5            panic(err)
 6        }
 7        defer file.Close()
 8
 9        scanner := bufio.NewScanner(file)
10        for scanner.Scan() {
11            if !yield(scanner.Text()) {
12                return
13            }
14        }
15        if err := scanner.Err(); err != nil {
16            panic(err)
17        }
18    }
19}

Pattern 2: Return Seq2 with Error

 1func LinesWithError(filename string) iter.Seq2[string, error] {
 2    return func(yield func(string, error) bool) {
 3        file, err := os.Open(filename)
 4        if err != nil {
 5            yield("", err)
 6            return
 7        }
 8        defer file.Close()
 9
10        scanner := bufio.NewScanner(file)
11        for scanner.Scan() {
12            if !yield(scanner.Text(), nil) {
13                return
14            }
15        }
16        if err := scanner.Err(); err != nil {
17            yield("", err)
18        }
19    }
20}
21
22// Usage
23for line, err := range LinesWithError("data.txt") {
24    if err != nil {
25        log.Fatal(err)
26    }
27    fmt.Println(line)
28}

Pattern 3: Separate Error Check

 1type LinesIter struct {
 2    filename string
 3    err      error
 4}
 5
 6func NewLinesIter(filename string) *LinesIter {
 7    return &LinesIter{filename: filename}
 8}
 9
10func (li *LinesIter) Iter() iter.Seq[string] {
11    return func(yield func(string) bool) {
12        file, err := os.Open(li.filename)
13        if err != nil {
14            li.err = err
15            return
16        }
17        defer file.Close()
18
19        scanner := bufio.NewScanner(file)
20        for scanner.Scan() {
21            if !yield(scanner.Text()) {
22                return
23            }
24        }
25        li.err = scanner.Err()
26    }
27}
28
29func (li *LinesIter) Err() error {
30    return li.err
31}
32
33// Usage
34linesIter := NewLinesIter("data.txt")
35for line := range linesIter.Iter() {
36    fmt.Println(line)
37}
38if err := linesIter.Err(); err != nil {
39    log.Fatal(err)
40}

Advanced Patterns

Stateful Iterator

 1type Counter struct {
 2    count int
 3}
 4
 5func (c *Counter) Next(n int) iter.Seq[int] {
 6    return func(yield func(int) bool) {
 7        for i := 0; i < n; i++ {
 8            c.count++
 9            if !yield(c.count) {
10                return
11            }
12        }
13    }
14}
15
16// Usage
17counter := &Counter{}
18for n := range counter.Next(3) {
19    fmt.Println(n) // 1, 2, 3
20}
21for n := range counter.Next(2) {
22    fmt.Println(n) // 4, 5
23}

Generator Pattern

 1func Generate[T any](generator func() (T, bool)) iter.Seq[T] {
 2    return func(yield func(T) bool) {
 3        for {
 4            val, ok := generator()
 5            if !ok {
 6                return
 7            }
 8            if !yield(val) {
 9                return
10            }
11        }
12    }
13}
14
15// Usage: random numbers
16rng := rand.New(rand.NewSource(time.Now().UnixNano()))
17randomInts := Generate(func() (int, bool) {
18    return rng.Intn(100), true
19})
20
21for n := range Take(randomInts, 5) {
22    fmt.Println(n)
23}

Lazy Evaluation

 1func LazyMap[V, U any](seq iter.Seq[V], f func(V) U) iter.Seq[U] {
 2    return func(yield func(U) bool) {
 3        for v := range seq {
 4            // f is only called when needed
 5            if !yield(f(v)) {
 6                return
 7            }
 8        }
 9    }
10}
11
12// Expensive operation only runs when values are consumed
13expensive := LazyMap(Count(1000000), func(n int) int {
14    time.Sleep(100 * time.Millisecond) // Expensive!
15    return n * n
16})
17
18// Only computes first 3 values
19for n := range Take(expensive, 3) {
20    fmt.Println(n)
21}

Limitations and Gotchas

1. No Direct Return of Error

Unlike channels, iterators can’t directly return errors. You must use one of the error handling patterns above.

2. Cleanup Must Be Explicit

 1func WithResource() iter.Seq[string] {
 2    return func(yield func(string) bool) {
 3        resource := acquireResource()
 4        defer resource.Close() // Always runs, even on break
 5
 6        for item := range resource.Items() {
 7            if !yield(item) {
 8                return // defer ensures cleanup
 9            }
10        }
11    }
12}

3. Pull Iterators Must Be Stopped

1next, stop := iter.Pull(someIterator)
2defer stop() // CRITICAL: prevents resource leaks
3
4// If you forget stop(), goroutines may leak

4. No Concurrency in yield

The yield function is not safe to call from multiple goroutines:

 1// ❌ WRONG: calling yield concurrently
 2func BadConcurrent() iter.Seq[int] {
 3    return func(yield func(int) bool) {
 4        var wg sync.WaitGroup
 5        for i := 0; i < 10; i++ {
 6            wg.Add(1)
 7            go func(n int) {
 8                defer wg.Done()
 9                yield(n) // NOT SAFE
10            }(i)
11        }
12        wg.Wait()
13    }
14}
15
16// ✅ CORRECT: collect then yield
17func GoodConcurrent() iter.Seq[int] {
18    return func(yield func(int) bool) {
19        ch := make(chan int, 10)
20        var wg sync.WaitGroup
21
22        for i := 0; i < 10; i++ {
23            wg.Add(1)
24            go func(n int) {
25                defer wg.Done()
26                ch <- n
27            }(i)
28        }
29
30        go func() {
31            wg.Wait()
32            close(ch)
33        }()
34
35        for n := range ch {
36            if !yield(n) {
37                return
38            }
39        }
40    }
41}

5. Performance Considerations

Comparison with Channels

FeatureIteratorsChannels
ConcurrencySequential onlyConcurrent safe
BufferingNo bufferingConfigurable buffer
CleanupAutomatic (defer)Manual (close)
ErrorsNo built-in supportCan send error values
PerformanceLower overheadHigher overhead
Use caseSequential pipelinesConcurrent communication

Real-World Example: Database Rows

 1type DB struct {
 2    // ...
 3}
 4
 5func (db *DB) Query(query string, args ...any) iter.Seq2[Row, error] {
 6    return func(yield func(Row, error) bool) {
 7        rows, err := db.queryInternal(query, args...)
 8        if err != nil {
 9            yield(Row{}, err)
10            return
11        }
12        defer rows.Close()
13
14        for rows.Next() {
15            var row Row
16            if err := rows.Scan(&row); err != nil {
17                yield(Row{}, err)
18                return
19            }
20            if !yield(row, nil) {
21                return
22            }
23        }
24
25        if err := rows.Err(); err != nil {
26            yield(Row{}, err)
27        }
28    }
29}
30
31// Usage
32for row, err := range db.Query("SELECT * FROM users WHERE age > ?", 18) {
33    if err != nil {
34        log.Fatal(err)
35    }
36    fmt.Printf("User: %s\n", row.Name)
37}

Range-over-func provides a clean, idiomatic way to create custom iteration logic in Go while maintaining the familiar range syntax and proper resource cleanup semantics.

#golang #programming #generics #syntax #coding #software #engineering #computer