You write a custom iterator and, inside its loop, call yield but don't bother checking what it returns — the loop body looks like it's just producing values, so what would there be to check? The consumer breaks out early, same as always. This time the program doesn't just stop: it panics, from inside the iterator's own internals, the next time it calls yield again.
func Evens(max int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := 0; i < max; i += 2 {
yield(i) // ignores the return value -- keeps going even after false
}
}
}
for n := range Evens(10) {
fmt.Println(n)
if n == 4 {
break
}
}
// 0
// 2
// 4
// panic: runtime error: range function continued iteration after function for loop body returned falserange has always worked over slices, arrays, maps, strings, channels, and (since Go 1.22) integers. Go 1.23 (released August 2024) added range-over-func: a for range loop can iterate over a function matching one of a few specific signatures, letting you define a custom, lazy sequence — skipping items, stopping early, or generating values on demand — without first materializing a slice.
range over integers (Go 1.22)
A smaller but handy addition: since Go 1.22, ranging over a plain integer counts from 0 up to (not including) that number, a shorthand for the classic counted for loop.
for i := range 5 {
fmt.Println(i) // 0, 1, 2, 3, 4
}range-over-func: custom iterators (Go 1.23)
A function can be ranged over if it matches one of the shapes func(yield func() bool), func(yield func(V) bool), or func(yield func(K, V) bool). Inside, you call yield with each value to produce; yield returns false if the loop stopped early (a break), letting the iterator clean up.
func Evens(max int) func(func(int) bool) {
return func(yield func(int) bool) {
for i := 0; i < max; i += 2 {
if !yield(i) {
return // consumer broke out of the loop early
}
}
}
}
for n := range Evens(10) {
fmt.Println(n) // 0, 2, 4, 6, 8
if n == 4 {
break // yield(6) would return false here
}
}Notice this Evens checks !yield(i) and returns as soon as it's true — that check is exactly what the callout at the top of this page leaves out. Skip it, and the iterator keeps calling yield after the consumer has already broken out of the loop, which the runtime treats as a programming error, not something to quietly ignore: it panics the very next time yield is called post-break, with a message that names the mistake directly rather than corrupting state silently.
The new iter package and standard library support
Go 1.23 added the iter package, defining the standard iter.Seq[V] and iter.Seq2[K, V] function types for iterators, and the slices and maps standard library packages gained iterator-returning functions (like maps.Keys, slices.Values) that plug directly into range-over-func, so custom collection types can now support idiomatic for range without exposing their internals as a slice first.