Skip to content
Go forJava Developers

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

On this page
  1. The shape of an iterator
  2. Iterators in the standard library
  3. Lazy pipelines vs Java streams
  4. Iterators that can fail
  5. Pull iterators

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

package iter: two function types
// 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.
Java — implementing Iterator
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 1
Go — returning an iter.Seq
func 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

JavaGo
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)
combining standard iterators
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

Java — Stream API
List<String> result = names.stream()
    .filter(s -> s.length() <= 3)
    .map(String::toUpperCase)
    .limit(2)
    .toList();
Go — small iterator adapters
// 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

Seq2[T, error] for paginated APIs and cursors
// 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

iter.Pull: turn a push iterator into next()
// 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
↑ ↓ to navigateEnter to openEsc to close