Iterators and Range Functions
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.goOutput:
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.