Go Range Over Functions

Golang Tutorials


Go 1.23 extended the for range statement so it can iterate over supported function types. This feature makes it possible to build user-defined iterators that work with the same range syntax already used for slices, maps, strings, channels, and integers.

The standard library also introduced the iter package, which defines common iterator function types such as iter.Seq and iter.Seq2. Together, these features give Go containers a consistent way to expose sequences without forcing every package to invent a different looping API.

Range Over Function Syntax

A range iterator is a function that accepts a yield callback. The yield function receives each value and returns a bool indicating whether iteration should continue.

// One-value iterator shape.
func(yield func(V) bool)

// Two-value iterator shape.
func(yield func(K, V) bool)

The iterator calls yield for each item. If yield returns false, the iterator must stop producing values and return.

When a for range loop exits early with break, Go causes the iterator's yield call to return false so the iterator can stop cleanly.

Create a Simple Iterator

The following function returns values from 1 through a requested limit. The loop stops after 3, so the iterator does not continue to 4 and 5.

Example:

package main

import "fmt"

// CountTo returns an iterator function.
func CountTo(n int) func(func(int) bool) {
    return func(yield func(int) bool) {
        for i := 1; i <= n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}

func main() {
    for value := range CountTo(5) {
        fmt.Println(value)
        if value == 3 {
            break
        }
    }
}

Output:

1
2
3

Use iter.Seq

The iter package gives the common one-value iterator type a standard name. iter.Seq[V] represents a sequence of values of type V.

Example:

package main

import (
    "fmt"
    "iter"
)

func Words() iter.Seq[string] {
    return func(yield func(string) bool) {
        for _, word := range []string{"plan", "build", "test"} {
            if !yield(word) {
                return
            }
        }
    }
}

func main() {
    for word := range Words() {
        fmt.Println(word)
    }
}

Output:

plan
build
test

Use iter.Seq2 for Pairs

iter.Seq2[K, V] is designed for iterators that produce two values at a time, such as an index and value or a key and value.

func Indexed(names []string) func(func(int, string) bool) {
    return func(yield func(int, string) bool) {
        for index, name := range names {
            if !yield(index, name) {
                return
            }
        }
    }
}

You can consume this function with the normal two-variable range form.

for index, name := range Indexed(names) {
    fmt.Println(index, name)
}

Standard Library Iterator Helpers

Go 1.23 added iterator-aware helpers to packages such as slices and maps. These functions make it easier to move between built-in collections and iterator sequences.

Helper Purpose
slices.Values() Iterate over slice values
slices.All() Iterate over index-value pairs
slices.Collect() Build a slice from an iterator
maps.Keys() Iterate over map keys
maps.Values() Iterate over map values
maps.All() Iterate over key-value pairs

Push and Pull Iterators

The range-compatible form is a push iterator: it pushes values to the yield callback. Go also provides iter.Pull() when code needs to request one value at a time instead of using a for range loop.

iter.Pull() returns a next function and a stop function. Call stop when you finish before consuming the whole sequence so the underlying iterator can clean up.

Write a Filter Adapter

Because iterators share a standard shape, one iterator can consume another and produce a filtered sequence.

func Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] {
    return func(yield func(V) bool) {
        for value := range seq {
            if keep(value) && !yield(value) {
                return
            }
        }
    }
}

Common Mistakes

  • Ignoring yield's return value: an iterator must stop when yield returns false.
  • Using an unsupported function signature: for range accepts specific iterator function shapes, not arbitrary functions.
  • Forgetting cleanup: iterators that hold files, network resources, or goroutines should release them when iteration stops early.
  • Making simple loops harder to read: an ordinary slice or map range is still clearer when no reusable iterator abstraction is needed.

When Range Over Functions Helps

  • Expose iteration for a custom tree, set, graph, or container.
  • Process a sequence lazily without first allocating a complete slice.
  • Build reusable adapters such as filter or transform functions.
  • Provide one familiar looping style across unrelated container implementations.

Best Practices

  • Use iter.Seq or iter.Seq2 in public APIs when their standard names improve clarity.
  • Return promptly when yield returns false.
  • Document iteration order when callers might depend on it.
  • Prefer direct loops for small local tasks where an iterator would add unnecessary abstraction.

Conclusion

Range over functions lets Go programs expose custom sequences through normal for range syntax. Combined with iter.Seq, iter.Seq2, and iterator-aware helpers in slices and maps, it provides a standard pattern for lazy and reusable iteration while keeping the loop at the call site simple.



Found This Page Useful? Share It!
Get the Latest Tutorials and Updates
Join us on Telegram