Iterators
Range-over-func iterators (Go 1.23+) in place of Iterable and Stream: writing them with yield, consuming them with for-range, and lazy pipelines.
Section 21 of 315 min read
For most of its life Go could only range over built-in types: slices, maps, strings, channels. Custom collections exposed callbacks or Next() methods, each with its own conventions. Go 1.23 added range-over-func: any function with the right shape can drive a for … range loop. The standard library now returns iterators from slices, maps, strings and bytes.
The shape of an iterator
// An iterator is a function that pushes values into yield.
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)
// yield returns false when the loop body executes break or return.
// The iterator must then stop immediately.class Countdown implements Iterable<Integer> {
private final int start;
Countdown(int start) { this.start = start; }
public Iterator<Integer> iterator() {
return new Iterator<>() {
int next = start;
public boolean hasNext() { return next > 0; }
public Integer next() {
if (next <= 0)
throw new NoSuchElementException();
return next--;
}
};
}
}
for (int n : new Countdown(3)) { ... } // 3 2 1func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return // the caller broke out of the loop
}
}
}
}
for n := range Countdown(3) {
fmt.Println(n) // 3 2 1
}Iterators in the standard library
| Java | Go |
|---|---|
list.iterator() | slices.Values(s), and slices.All(s) for index + value |
list.reversed() (Java 21) | slices.Backward(s) |
map.keySet() / map.values() | maps.Keys(m) / maps.Values(m) |
stream.toList() | slices.Collect(seq) |
stream.sorted().toList() | slices.Sorted(seq) |
string.lines() | strings.Lines(s) (Go 1.24) |
Pattern.compile(",").splitAsStream(s) | strings.SplitSeq(s, ",") (Go 1.24) |
IntStream.range(0, n) | for i := range n (Go 1.22, a plain loop) |
ages := map[string]int{"bob": 31, "ada": 36}
// Sorted keys: iterate a map in a stable order
for _, name := range slices.Sorted(maps.Keys(ages)) {
fmt.Println(name, ages[name])
}
// Split lazily, without allocating a []string
for field := range strings.SplitSeq("a,b,c", ",") {
fmt.Println(field)
}Lazy pipelines vs Java streams
List<String> result = names.stream()
.filter(s -> s.length() <= 3)
.map(String::toUpperCase)
.limit(2)
.toList();// Adapters like this are about ten lines each.
// The standard library does not ship Filter/Map for iter.Seq.
func Filter[V any](seq iter.Seq[V], keep func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for v := range seq {
if keep(v) && !yield(v) {
return
}
}
}
}
short := Filter(slices.Values(names), func(s string) bool {
return len(s) <= 3
})
// Map and Take are written the same way. Nothing runs until
// Collect pulls values through the pipeline.
result := slices.Collect(Take(Map(short, strings.ToUpper), 2))Iterators that can fail
// Pages through an API lazily. Callers see one flat sequence.
func (c *Client) Users(ctx context.Context) iter.Seq2[User, error] {
return func(yield func(User, error) bool) {
cursor := ""
for {
page, err := c.fetchPage(ctx, cursor)
if err != nil {
yield(User{}, err) // report the error once, then stop
return
}
for _, u := range page.Users {
if !yield(u, nil) {
return // caller stopped early: no more requests
}
}
if page.Next == "" {
return
}
cursor = page.Next
}
}
}
for u, err := range client.Users(ctx) {
if err != nil {
return fmt.Errorf("list users: %w", err)
}
if u.Active {
notify(u)
}
}Pull iterators
// When you need Java-style next() control, for example to
// merge two sorted sequences step by step:
next, stop := iter.Pull(Countdown(2))
defer stop() // always release the iterator's resources
v, ok := next() // 2 true
v, ok = next() // 1 true
v, ok = next() // 0 false: exhausted