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:
- Returns
true: Consumer wants more values (continue iterating) - Returns
false: Consumer stopped (break, return, or finished)
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:
- Manual control over iteration timing
- To iterate multiple sequences in parallel
- To implement complex control flow
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
- Iterator functions have minimal overhead (inlined in many cases)
- Pull iterators allocate a goroutine and channel internally
- Deeply nested adapters may impact performance in hot paths
Comparison with Channels
| Feature | Iterators | Channels |
|---|---|---|
| Concurrency | Sequential only | Concurrent safe |
| Buffering | No buffering | Configurable buffer |
| Cleanup | Automatic (defer) | Manual (close) |
| Errors | No built-in support | Can send error values |
| Performance | Lower overhead | Higher overhead |
| Use case | Sequential pipelines | Concurrent 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