Iterators and Range Functions

Updated

September 13, 2026

Iterators and Range Functions

Since Go 1.23, you can use for range over functions, enabling standard iterators for any custom collection or stream.

Mental model

Since Go 1.23, range can iterate over a function with signature func(yield func(V) bool) (which is iter.Seq[V]) or func(yield func(K, V) bool) (which is iter.Seq2[K,V]). The function calls yield for each element; if yield returns false, iteration stops early (e.g. a break). iter.Seq[V] and iter.Seq2[K,V] are type aliases in the iter package for these signatures. slices.Collect(seq) drains a Seq into a slice. maps.Collect(seq2) drains a Seq2 into a map.

Worked examples

Case 1: iter.Seq[int]

A Tickets function returns an iterator over a slice. The closure calls yield per ID.

// tickets.go
package main

import (
    "fmt"
    "iter"
)

func Tickets(ids []int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, id := range ids {
            if !yield(id) {
                return
            }
        }
    }
}

func main() {
    for id := range Tickets([]int{7, 8, 9}) {
        fmt.Printf("Processing ticket %d\n", id)
    }
}

Run:

go run tickets.go

Output:

Processing ticket 7
Processing ticket 8
Processing ticket 9

Case 2: iter.Seq2[string, int]

Prices yields key/value pairs from a menu map.

// prices.go
package main

import (
    "fmt"
    "iter"
)

func Prices(menu map[string]int) iter.Seq2[string, int] {
    return func(yield func(string, int) bool) {
        for item, price := range menu {
            if !yield(item, price) {
                return
            }
        }
    }
}

func main() {
    menu := map[string]int{
        "coffee": 250,
        "tea":    200,
    }

    for item, price := range Prices(menu) {
        fmt.Printf("%s: %dc\n", item, price)
    }
}

Run:

go run prices.go

Output:

coffee: 250c
tea: 200c

Case 3: Early termination

An iterator that yields tables with free capacity. The caller breaks after finding the first one.

// tables.go
package main

import (
    "fmt"
    "iter"
)

func AllOpen(tables []int, capacity int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, seats := range tables {
            if seats >= capacity {
                if !yield(seats) {
                    return
                }
            }
        }
    }
}

func main() {
    tables := []int{2, 1, 4, 6, 2}
    
    for table := range AllOpen(tables, 4) {
        fmt.Printf("Found table with %d seats. Booking it!\n", table)
        break // This will cause yield to return false
    }
}

Run:

go run tables.go

Output:

Found table with 4 seats. Booking it!

Case 4: slices.Collect

Collect the Tickets iterator into a []int slice using the slices package.

// collect.go
package main

import (
    "fmt"
    "iter"
    "slices"
)

func Tickets(ids []int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, id := range ids {
            if !yield(id) {
                return
            }
        }
    }
}

func main() {
    seq := Tickets([]int{101, 102, 103})
    
    // Drain the iterator into a slice
    all := slices.Collect(seq)
    
    fmt.Printf("Collected tickets: %v\n", all)
}

Run:

go run collect.go

Output:

Collected tickets: [101 102 103]

Case 5: Composing iterators

A Filter that wraps another iterator.

// filter.go
package main

import (
    "fmt"
    "iter"
    "slices"
)

func Tickets(ids []int) iter.Seq[int] {
    return func(yield func(int) bool) {
        for _, id := range ids {
            if !yield(id) {
                return
            }
        }
    }
}

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) {
                if !yield(v) {
                    return
                }
            }
        }
    }
}

func main() {
    seq := Tickets([]int{1, 2, 3, 4, 5})
    
    evens := Filter(seq, func(v int) bool {
        return v%2 == 0
    })
    
    fmt.Printf("Evens: %v\n", slices.Collect(evens))
}

Run:

go run filter.go

Output:

Evens: [2 4]

The trap

Calling yield after it returned false. Always return immediately when yield returns false; failing to do so is a bug and the runtime might panic or exhibit weird behavior.

// BAD pattern:
func BadSeq() iter.Seq[int] {
    return func(yield func(int) bool) {
        yield(1)
        yield(2) // BUG: if yield(1) returned false, this shouldn't be called!
    }
}

// GOOD pattern:
func GoodSeq() iter.Seq[int] {
    return func(yield func(int) bool) {
        if !yield(1) { return }
        if !yield(2) { return }
    }
}

The boring rule

Use standard iter.Seq and iter.Seq2 when you want to abstract over a collection or stream. Always check the return value of yield and return immediately if it is false.

Try this

Write an iter.Seq2[int, string] function called Enumerate that wraps a iter.Seq[string] and yields (0, val), (1, val), etc. Use it to print a numbered list of ticket statuses.