Iterators and range-over-func Deep Dive
Iterators and range-over-func Deep Dive
Overview
Go 1.22–1.23 made for range over functions a language feature, with package iter defining Seq / Seq2. This is a deep dive on semantics and costs — not just syntax.
Also: slices/maps/cmp/iter, loops.
Diagram: Push iterator
flow:
[Yield] --true--> [Next]
[Yield] --false--> [Stop]
Types
type Seq[V any] func(yield func(V) bool)
type Seq2[K, V any] func(yield func(K, V) bool)The iterator pushes values into yield. Returning false from yield stops early (break).
Desugaring Intuition
for v := range All(s) {
if v < 0 {
break
}
}
// roughly calls All(s)(func(v T) bool { ...; return continue? })Panic safety: iterator code should treat yield like a callback that may not return normally if the consumer panics — stdlib iterators are careful; custom ones should be too.
Pull Iterators
next, stop := iter.Pull(seq)
defer stop()
for {
v, ok := next()
if !ok {
break
}
}Pull converts push-style to pull-style via a small coroutine-like parking protocol — convenient adapters, slight overhead.
When Iterators Shine
- Lazy pipelines over large or infinite streams
- Library APIs that used to allocate intermediate slices
- Single implementation for
rangeandslices.Collect
When They Don’t
- Tiny hot loops where a classic index for is clearer and fully BCE-friendly
- Need for random access
- Over-abstracting one-off loops
Experiment
go mod init example
go run .package main
import (
"fmt"
"iter"
"slices"
)
func Filter[V any](s []V, ok func(V) bool) iter.Seq[V] {
return func(yield func(V) bool) {
for _, v := range s {
if !ok(v) {
continue
}
if !yield(v) {
return
}
}
}
}
func main() {
nums := []int{1, 2, 3, 4, 5}
for v := range Filter(nums, func(n int) bool { return n%2 == 0 }) {
fmt.Println("even", v)
}
fmt.Println("collect", slices.Collect(Filter(nums, func(n int) bool { return n > 3 })))
}What to notice: No intermediate filtered slice until Collect.
Try next: Implement Map and chain Map(Filter(...)); compare allocs to nested slice loops.