Iterating maps
range works on maps the same way it works on slices and strings, but with one surprising rule: the iteration order is random, and it changes between runs of the same program. This lesson covers how to range over a map and, more importantly, how to get deterministic output when you need it.
The random-order rule
Ranging over a map yields key-value pairs:
ages := map[string]int{"alice": 30, "bob": 25, "carol": 40}
for k, v := range ages {
fmt.Println(k, v)
}
One run might print:
alice 30
bob 25
carol 40
Another run of the same program might print:
carol 40
alice 30
bob 25
or any of the other permutations. Go deliberately shuffles the starting position every time. The language spec does not promise an order, and the runtime takes advantage of that freedom to randomise what you get.
This is not an accident or an oversight. If the order were stable, developers would start to rely on it, and code would break mysteriously when a map internal changed in a future Go version. Randomising from day one keeps every codebase honest.
Getting a deterministic order
When you need predictable iteration (printing a report, producing stable test output, writing to a log), the pattern is: pull the keys into a slice, sort the slice, then iterate through the sorted slice and read the map.
In modern Go (1.21+), the one-liner for that is slices.Sorted(maps.Keys(m)):
import (
"maps"
"slices"
)
ages := map[string]int{"alice": 30, "bob": 25, "carol": 40}
for _, k := range slices.Sorted(maps.Keys(ages)) {
fmt.Println(k, ages[k])
}
Output, stable across every run:
alice 30
bob 25
carol 40
maps.Keys(m) produces an iterator over the keys, slices.Sorted collects them into a slice and sorts it. The whole "collect then sort" step is one expression.
What that expands to
The slices.Sorted(maps.Keys(...)) one-liner is a thin wrapper over the pattern that existed before Go 1.21. Seeing it written out makes the shape visible, and you will still meet it in plenty of existing Go code:
import "sort"
keys := make([]string, 0, len(ages))
for k := range ages {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, ages[k])
}
Three things worth noticing in the expanded form:
for k := range agestakes just the keys. This is the map equivalent of the "index-only" single-variable form you met in the for-range lesson.sort.Stringssorts the slice in place.sortis the pre-generics legacy package;slices.Sortis the modern generic replacement.- The read inside the loop is
ages[k]. You rebuild the pairing using the sorted keys slice and the map itself. There is no way to sort a map directly, because maps have no order; the sort lives on the slice of keys you built.
make([]string, 0, len(ages)) pre-sizes the backing array for the keys slice to exactly len(ages) elements. You already know how many keys there are, so there is no reason to let append reallocate multiple times as it grows. This is the "pre-allocate when you know the target size" pattern from the make lesson earlier in this chapter.
In new Go code, slices.Sorted(maps.Keys(m)) is the one-liner. Mentally, think of it as "collect the keys, sort them". You will reach for this shape in dozens of places: tests with stable output, CLI tools that print reports, log formatters, debug dumps.
The editor on this site has limited support for generics in standard-library functions, so slices.Sorted and maps.Keys will not currently run here. The exercise uses the expanded form with sort.Strings so you can run it. On your own machine with a normal go build, the slices.Sorted(maps.Keys(m)) one-liner is the form to reach for.
Starter declares counts := map[string]int{"go": 2, "rust": 1, "python": 3, "javascript": 1}.
- Collect the keys into a
[]stringslice, pre-sized withmake. - Sort the slice with
sort.Strings. - Range over the sorted slice and print
"key: value"for each pair, usingPrintfwith%s: %d\n.
Expected output, deterministic:
go: 2
javascript: 1
python: 3
rust: 1
Remember to add "sort" to your imports.
go: 2 javascript: 1 python: 3 rust: 1