languages
September 11, 2026 · 9 min read · 6 views

Go 1.24: Range Over Functions – A Game-Changer for Iterator Patterns

Go 1.24 introduces range-over-func, enabling custom iterators with familiar for-range syntax. Learn how this feature transforms iteration patterns and simplifies complex data structures.

Understanding Go’s Iterator Evolution

Go 1.24, released in February 2025, introduces one of the most anticipated features in recent Go history: range over functions. This capability allows developers to write custom iterators that work seamlessly with Go’s familiar for-range loop syntax, eliminating boilerplate and making code more idiomatic.

For years, Go developers working with complex data structures or lazy-loaded sequences faced a choice: use channels (which introduce concurrency overhead and buffer management), build custom helper functions, or accept verbose iteration logic. Range over functions changes this equation entirely.

What Are Range Over Functions?

Range over functions, formally known as “iter.Seq,” allow you to define a function that yields values one at a time to a callback. The Go compiler automatically makes these compatible with for-range loops.

Before Go 1.24, iterating over a custom collection looked like this:

type IntList struct {
    items []int
}

func (l *IntList) Iterate(callback func(int) bool) {
    for _, item := range l.items {
        if !callback(item) {
            break
        }
    }
}

func main() {
    list := &IntList{items: []int{1, 2, 3, 4, 5}}
    list.Iterate(func(item int) bool {
        fmt.Println(item)
        return true
    })
}

With Go 1.24, the same functionality becomes:

import "iter"

type IntList struct {
    items []int
}

func (l *IntList) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, item := range l.items {
            if !yield(item) {
                return
            }
        }
    }
}

func main() {
    list := &IntList{items: []int{1, 2, 3, 4, 5}}
    for item := range list.All() {
        fmt.Println(item)
    }
}

The difference is subtle but powerful: developers can now use the standard for-range loop without worrying about callback signatures or manual break logic.

The Iter Package and Type Signatures

Go 1.24 introduces two key types in the iter package:

iter.Seq[V]

For single-value iteration:

type Seq[V any] func(yield func(V) bool)

This generates values of type V one at a time. The yield function returns false when the caller breaks from the loop.

iter.Seq2[K, V]

For key-value iteration (useful for maps, indexed collections):

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

Step-by-Step Guide: Building Custom Iterators

Example 1: Iterating Over a Binary Tree

Let’s build a practical example—in-order traversal of a binary search tree:

package main

import (
    "fmt"
    "iter"
)

type Node struct {
    Value int
    Left  *Node
    Right *Node
}

func (n *Node) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        n.inOrder(yield)
    }
}

func (n *Node) inOrder(yield func(int) bool) {
    if n == nil {
        return
    }
    if !n.Left.All()(yield) {
        return
    }
    if !yield(n.Value) {
        return
    }
    if !n.Right.All()(yield) {
        return
    }
}

func main() {
    // Build tree:     4
    //                / \
    //               2   6
    //              / \ / \
    //             1 3 5 7
    tree := &Node{
        Value: 4,
        Left: &Node{
            Value: 2,
            Left:  &Node{Value: 1},
            Right: &Node{Value: 3},
        },
        Right: &Node{
            Value: 6,
            Left:  &Node{Value: 5},
            Right: &Node{Value: 7},
        },
    }

    for val := range tree.All() {
        fmt.Print(val, " ") // Output: 1 2 3 4 5 6 7
    }
}

Example 2: Lazy Range Generator

Here’s a function that generates a range lazily (only computing values when needed):

package main

import (
    "fmt"
    "iter"
)

func Range(start, end int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; i < end; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    // Iterate from 0 to 9
    for i := range Range(0, 10) {
        fmt.Println(i)
    }

    // Break early (efficient—stops generating)
    for i := range Range(0, 1000000) {
        if i == 10 {
            break
        }
        fmt.Println(i)
    }
}

Example 3: Key-Value Iteration Over a Map Wrapper

Using iter.Seq2 for collections that support key-value pairs:

package main

import (
    "fmt"
    "iter"
)

type StringMap struct {
    data map[string]int
}

func (m *StringMap) All() iter.Seq2[string, int] {
    return func(yield func(string, int) bool) {
        for k, v := range m.data {
            if !yield(k, v) {
                return
            }
        }
    }
}

func main() {
    m := &StringMap{
        data: map[string]int{
            "alice": 30,
            "bob":   25,
            "carol": 28,
        },
    }

    for name, age := range m.All() {
        fmt.Printf("%s is %d years old\n", name, age)
    }
}

Advanced Patterns: Chaining Iterators

One of the most powerful aspects of range-over-func is composition. You can easily chain operations:

package main

import (
    "fmt"
    "iter"
)

// Filter creates a new iterator that yields only values where predicate is true
func Filter[V any](seq iter.Seq[V], predicate func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for v := range seq {
            if predicate(v) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

// Map transforms each value in the sequence
func Map[V, U any](seq iter.Seq[V], transform func(V) U) iter.Seq[U] {
    return func(yield func(U) bool) {
        for v := range seq {
            if !yield(transform(v)) {
                return
            }
        }
    }
}

func Range(start, end int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; i < end; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    // Chain: Range → Filter (even numbers) → Map (multiply by 2)
    seq := Map(
        Filter(
            Range(0, 20),
            func(n int) bool { return n%2 == 0 },
        ),
        func(n int) int { return n * 2 },
    )

    for val := range seq {
        fmt.Println(val) // 0, 4, 8, 12, 16, 20, 24, 28, 32, 36
    }
}

Performance Implications

Range over functions are not a zero-cost abstraction, but they’re highly optimized:

  • No heap allocations for the iterator state (inline functions)
  • Compiler inlining ensures minimal overhead in hot paths
  • Lazy evaluation prevents unnecessary computation
  • Early termination works efficiently—breaking from the loop stops generator execution immediately

Benchmarks show that range-over-func performance is comparable to hand-written loop code, and significantly faster than channel-based iteration.

Comparison with Previous Approaches

Channels (Pre-1.24)

func (l *IntList) Channel() <-chan int {
    ch := make(chan int)
    go func() {
        defer close(ch)
        for _, item := range l.items {
            ch <- item
        }
    }()
    return ch
}

// Usage
for item := range list.Channel() { }

Drawbacks: Goroutine overhead, channel buffer management, potential leaks if not fully consumed.

Custom Visitor Pattern (Pre-1.24)

func (l *IntList) ForEach(fn func(int)) {
    for _, item := range l.items {
        fn(item)
    }
}

// Usage
list.ForEach(func(item int) { /* process */ })

Drawbacks: Can’t use break cleanly, awkward for complex control flow.

Range Over Functions (1.24+)

func (l *IntList) All() iter.Seq[int] { /* ... */ }

// Usage
for item := range list.All() { }

Advantages: Standard syntax, early termination, composable, idiomatic.

Common Pitfalls and Best Practices

Pitfall 1: Forgetting to Check the yield Return Value

❌ Wrong:

func (l *IntList) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, item := range l.items {
            yield(item) // Ignoring return value!
        }
    }
}

✅ Correct:

func (l *IntList) All() iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, item := range l.items {
            if !yield(item) {
                return // Caller broke, stop generating
            }
        }
    }
}

Pitfall 2: Not Understanding Lazy Evaluation

Range-over-func is lazy by design. Values are only generated when the caller requests them. This is intentional and powerful, but can surprise developers familiar with eager evaluation:

func ExpensiveRange(start, end int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for i := start; i < end; i++ {
            fmt.Printf("Computing %d\n", i) // Only prints when loop runs
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    seq := ExpensiveRange(0, 1000000)
    // Nothing is computed yet—seq is just a function

    for i := range seq {
        if i == 5 {
            break
        }
        fmt.Println(i)
    }
    // Only computed 0–5, not the entire range
}

Best Practice: Name Your Iterator Methods Consistently

The Go community is converging on All() as the method name for single-value iterators:

func (c *Collection) All() iter.Seq[Item] { }

For key-value, use All() as well but return iter.Seq2. For backward-compatibility or different iteration orders, suffix with a variant:

func (t *Tree) All() iter.Seq[Value] { } // In-order
func (t *Tree) AllDescending() iter.Seq[Value] { } // Reverse order

Testing Iterators

Range-over-func makes it easy to test custom iteration logic:

package main

import (
    "iter"
    "testing"
)

func TestIntListIteration(t *testing.T) {
    list := &IntList{items: []int{1, 2, 3}}
    var result []int
    for item := range list.All() {
        result = append(result, item)
    }

    expected := []int{1, 2, 3}
    if len(result) != len(expected) {
        t.Fatalf("expected %v, got %v", expected, result)
    }
    for i, v := range result {
        if v != expected[i] {
            t.Errorf("item %d: expected %d, got %d", i, expected[i], v)
        }
    }
}

Integration with Go’s Standard Library

Go 1.24 updates the standard library to support range-over-func where appropriate. The slices package now includes:

import "slices"

func All[E any](s []E) iter.Seq[E]
func Backward[E any](s []E) iter.Seq[E]

Maps, once iterable only with unordered range, now support:

import "maps"

func All[M ~map[K]V, K comparable, V any](m M) iter.Seq2[K, V]
func Keys[M ~map[K]V, K comparable, V any](m M) iter.Seq[K]
func Values[M ~map[K]V, K comparable, V any](m M) iter.Seq[V]

Why It Matters for Your Codebase

Range over functions represent a significant shift in Go’s design philosophy. By enabling custom iterators that integrate seamlessly with language syntax, Go eliminates friction points that developers have worked around for years.

Impact on Code Quality:

  • Reduces boilerplate and improves readability
  • Enables functional composition patterns without overhead
  • Simplifies library design for data structure implementations
  • Makes code more testable by removing callback complexity

Ecosystem Evolution: Expect third-party libraries to adopt range-over-func extensively. Database drivers, query builders, and collection libraries will likely provide All() methods, making iteration patterns uniform across dependencies.

Getting Started with Go 1.24

To start using range-over-func:

  1. Update Go: Download Go 1.24+ from golang.org
  2. Update go.mod: Set go 1.24 in your module file
  3. Import iter: Use import "iter" in files defining custom iterators
  4. Define iterator methods: Implement All() iter.Seq[T] or All() iter.Seq2[K, V]
  5. Use for-range: Iterate over your custom types with standard loop syntax

Testing Your Iterator Implementation

Use Kloubot’s tools to validate iteration logic in tests. The Regex Tester can help verify patterns generated by iterators, and the Mock Data Generator makes it easy to create test fixtures.

Common Questions

Q: Should I replace all my channels with range-over-func? A: Not always. Channels are still valuable for concurrent communication. Use range-over-func for synchronous iteration over collections, channels for goroutine communication.

Q: Can I use range-over-func with generic types? A: Yes, fully. iter.Seq[T] and iter.Seq2[K, V] work seamlessly with Go’s type parameters.

Q: What about breaking early from an iterator? A: Built-in—use break normally in the for-range loop. The generator receives false from yield and stops executing.

Conclusion

Go 1.24’s range-over-func feature elevates Go’s approach to iteration by merging syntactic familiarity with compositional power. For developers building libraries, data structures, or complex iteration patterns, this feature eliminates boilerplate while maintaining Go’s performance and simplicity standards.

Start experimenting with custom iterators in your projects. The idiom will become as natural as using range over slices and maps.

This post was generated with AI assistance and reviewed for accuracy.