Dev Skills

Iterators and range over functions

What range can loop over since Go 1.22 and 1.23, and why maps.Keys returns a function.

Topic 45 of 98 · 5 min to read · 45 min to build
After this topic you can
  • Write a loop with range over an integer.
  • Write an iter.Seq[int] function that stops when yield returns false.
  • Predict what printing maps.Keys(m) shows, and fix it with slices.Sorted.

range over an int

Since Go 1.22, range works on a plain integer. for i := range 5 counts 0 to 4. It is the three-part loop with less to get wrong.

range-int.go
package main
import "fmt"
func main() {
for i := range 3 {
fmt.Println("attempt", i+1)
}
}

What an iterator is

An iterator is a function that produces values one at a time by calling a function you give it, called yield. When yield returns false, the loop has stopped (a break, say), and the iterator must stop too.

Since Go 1.23 you can range over such a function directly. The iter package names the two shapes: iter.Seq[V] gives one value per step, iter.Seq2[K, V] gives two, like a map’s key and value.

countdown.go
package main
import (
"fmt"
"iter"
)
// Countdown yields n, n-1, ... 1.
func Countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) { // the loop said stop
return
}
}
}
}
func main() {
for v := range Countdown(5) {
if v == 2 {
break // yield returns false, and Countdown returns
}
fmt.Println(v)
}
}
Forget the `if !yield(...)` check and the program panics when the loop breaks early. Always check it.

Two values per step: iter.Seq2

seq2.go
package main
import (
"fmt"
"iter"
)
// Lines yields each line number with its text.
func Lines(lines []string) iter.Seq2[int, string] {
return func(yield func(int, string) bool) {
for i, l := range lines {
if !yield(i+1, l) {
return
}
}
}
}
func main() {
for n, text := range Lines([]string{"package main", "", "func main() {}"}) {
fmt.Printf("%d: %q\n", n, text)
}
}

The standard library speaks iterators

maps.Keys(m) and maps.Values(m) return an iter.Seq, not a slice, so nothing is copied until you ask. slices.Collect turns any iter.Seq into a slice, and slices.Sorted collects and sorts in one step. slices.All and slices.Values go the other way, turning a slice into an iterator.

stdlib-iter.go
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
stock := map[string]int{"pears": 3, "apples": 5, "figs": 1}
keys := slices.Sorted(maps.Keys(stock)) // sorted []string, every run
fmt.Println(keys)
counts := slices.Collect(maps.Values(stock)) // a []int, in random order
fmt.Println(len(counts), "values")
for _, k := range keys {
fmt.Println(k, stock[k])
}
}
`slices.Sorted(maps.Keys(m))` is the one-line answer to "print a map in order".
Note: Do not turn every loop into an iterator. Return a slice when the caller wants all the values anyway. Iterators earn their place when the sequence is large, endless, or expensive to build, or when the caller may stop early.
The short version
  • for i := range n counts from 0 to n-1 (Go 1.22).
  • An iterator is a function that calls yield for each value, and stops when yield returns false (Go 1.23).
  • iter.Seq[V] yields one value per step, iter.Seq2[K, V] yields two.
  • maps.Keys returns an iterator. slices.Collect and slices.Sorted turn one into a slice.
Remember: An iterator is just a function you can range over. Always check what yield returns.
Build this before moving on
Write func Fib() iter.Seq[int] that yields Fibonacci numbers forever. Range over it and break after the first value above 1000. Then write Evens(seq iter.Seq[int]) iter.Seq[int] that filters another iterator.
Coding challenge · checked by tests
Write Evens(seq iter.Seq[int]) iter.Seq[int] that yields only the even numbers from seq, and stops when yield returns false. Examples: 1 2 3 4 → 2 4, 1 3 5 → (nothing), 0 -2 7 → 0 -2.

Tab indents. Press Esc, then Tab, to leave the editor.

Then try these in your own editor
  1. 1Remove the if !yield(i) check from Countdown, call yield(i) on its own, and break out of the loop early.
  2. 2Print maps.Keys(m) directly with fmt.Println.
  3. 3Loop over slices.All([]string{"a", "b"}) and print what you get each step.
Check yourselfTake the Generics and stdlib test