Go

How do range-over-func iterators (Go 1.23) work? What does this print?

Question 36HardGo 1.22 to 1.25
func Countdown(n int) iter.Seq[int] { // iter.Seq[V] is func(yield func(V) bool)
	return func(yield func(int) bool) {
		defer fmt.Println("iterator cleanup")
		for i := n; i >= 0; i-- {
			if !yield(i) {
				return // the consumer stopped early
			}
		}
	}
}

func main() {
	for v := range Countdown(5) {
		if v == 2 {
			break
		}
		fmt.Print(v, " ")
	}
	fmt.Println("after loop")
}

Output:

5 4 3 iterator cleanup
after loop

Since Go 1.23, for range accepts functions of the form func(yield func() bool), func(yield func(V) bool) and func(yield func(K, V) bool). The compiler turns the loop body into the yield callback. yield returns true to ask for more values. A break, return, goto or panic out of the body makes yield return false. The iterator must then stop, and its own defers run before the loop statement finishes. That is why "iterator cleanup" prints before "after loop".

Rules and gotchas:

  • An iterator that calls yield again after it returned false triggers a run-time panic: range function continued iteration after function for loop body returned false. Always check the result of yield.
  • return, labeled break/continue and defer inside the loop body keep their normal meaning. A defer in the body belongs to the enclosing function, not to the iterator.
  • These are "push" iterators. iter.Pull converts one into a "pull" iterator (next, stop), which is useful for zipping two sequences. Always call stop.
  • The standard library provides them: slices.All, slices.Values, slices.Backward, maps.Keys, maps.All, and in Go 1.24 strings.Lines, strings.SplitSeq and bytes.SplitSeq.
  • The loop variable follows the Go 1.22 per-iteration rules, so capturing it in a closure is safe.

More on Language Fundamentals & Types

All 39 Language Fundamentals & Types questions