Go

How do range-over-func iterators work on slices and maps, and what happens if an iterator ignores yield's return value?

Question 75HardGo 1.22 to 1.25

Since Go 1.23 a for range loop can take a function of type func(yield func(K, V) bool) (that is, iter.Seq2[K, V]), func(yield func(V) bool) (iter.Seq[V]) or func(yield func() bool). The compiler turns the loop body into the yield function. yield returns false when the body executes break or return, and the iterator must stop at that point.

for i, v := range slices.Backward([]string{"a", "b", "c"}) {
	if i == 0 {
		break
	}
	fmt.Printf("%d%s ", i, v)
}
fmt.Println() // prints: 2c 1b

// A broken iterator: ignores yield's result
func bad(yield func(int) bool) {
	for i := range 3 {
		yield(i)
	}
}

for v := range bad {
	if v == 1 {
		break
	}
}
// panic: runtime error: range function continued iteration after function for loop body returned false

The correct pattern checks every call: if !yield(v) { return }. The runtime checks this contract and panics instead of running the loop body again after a break. That keeps break, return, goto and defer inside the loop body working as they do in ordinary loops.

Other points worth knowing:

  • Iterators are usually lazy and single-pass. maps.Keys(m) reads the live map, so changes to the map during iteration follow the normal map rules.
  • A defer inside the loop body runs when the enclosing function returns, not when the iterator returns.
  • Pull-style use is available through iter.Pull, which returns next and stop functions. Always call stop (usually with defer), or the iterator's goroutine-like state leaks.
  • Useful Go 1.23 helpers: slices.All, Values, Backward, Collect, AppendSeq, Sorted, Chunk. Go 1.24 added strings.Lines, SplitSeq and FieldsSeq, which split without building a []string.

More on Arrays, Slices, Maps & Strings

All 37 Arrays, Slices, Maps & Strings questions