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.