Map Iteration Order
Ranging over a map visits keys in a random order on purpose; sort the keys first whenever the output must be the same every run.
for _, k := range slices.Sorted(maps.Keys(m)) { use(k, m[k]) }Ranging over a map gives you its keys in no particular order. The order is not the insertion order and not sorted. The Go specification says the order is unspecified, and the runtime goes further: it starts each iteration at a random point, so the same map ranged twice in the same program usually comes out in two different orders.
Why it is deliberate
If the order happened to be stable, code would quietly start depending on it. That code would then break whenever the map implementation changed, or whenever a few more keys were inserted. Randomising makes the dependency fail immediately, while you are writing it, instead of a year later in production.
Getting a stable order
Collect the keys, sort them, then range over the sorted keys and look each value up. The longhand is a loop that appends every key into a slice followed by slices.Sort. The shorthand is slices.Sorted(maps.Keys(m)), which does the same two steps in one call; maps.Keys in its iterator form and slices.Sorted were both added in Go 1.23, and the slices package itself in Go 1.21.
One exception: printing a whole map with fmt is already stable. Since Go 1.12 fmt sorts map keys itself before printing, so fmt.Println(m) is deterministic. Only your own range loops are random.
What trips people up
Changing a map while ranging it. Removing an entry you have not reached yet means it will not be produced. Adding an entry may or may not be visited, so never rely on either outcome.
Example
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
stock := map[string]int{"pear": 4, "apple": 7, "fig": 2}
// Shorthand: collect the keys and sort them in one call (Go 1.23).
keys := slices.Sorted(maps.Keys(stock))
fmt.Println(keys)
for _, k := range keys {
fmt.Printf("%s=%d\n", k, stock[k])
}
// Longhand for the same two steps, and what older code looks like.
older := make([]string, 0, len(stock))
for k := range stock {
older = append(older, k)
}
slices.Sort(older)
fmt.Println(older)
// Printing a whole map is already stable: fmt sorts the keys itself.
fmt.Println(stock)
}
// Output:
// [apple fig pear]
// apple=7
// fig=2
// pear=4
// [apple fig pear]
// map[apple:7 fig:2 pear:4]
When to use it
- Print configuration or metrics in the same order every run.
- Write a test whose expected output is a fixed string.
- Produce a top-N list from a map of counters.
- Evict expired entries from a cache map while walking it.
More examples
Deterministic output for a report
Any function whose output is compared, in a test or by a human reading a diff, must fix its own order. Sorting the keys inside the function means every caller gets the same bytes.
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
// Builds one line per key, in a fixed order, so two runs agree.
func render(fields map[string]string) string {
var b strings.Builder
for _, k := range slices.Sorted(maps.Keys(fields)) {
b.WriteString(k)
b.WriteString(": ")
b.WriteString(fields[k])
b.WriteString("\n")
}
return b.String()
}
func main() {
fmt.Print(render(map[string]string{
"status": "ok",
"region": "eu-west-1",
}))
}
// Output:
// region: eu-west-1
// status: ok
Ordering by value instead of by key
Top-N lists need two sorts: one to break ties predictably and one for the real ordering. slices.SortStableFunc takes a function returning a negative number, zero or a positive number, and cmp.Compare produces exactly that. Both arrived in Go 1.21.
package main
import (
"cmp"
"fmt"
"maps"
"slices"
)
func main() {
counts := map[string]int{"go": 3, "rust": 3, "c": 1}
// Start from alphabetical order.
keys := slices.Sorted(maps.Keys(counts))
// Then re-sort by count, highest first. A stable sort keeps the
// alphabetical order for keys with equal counts.
slices.SortStableFunc(keys, func(a, b string) int {
return cmp.Compare(counts[b], counts[a])
})
for _, k := range keys {
fmt.Println(k, counts[k])
}
}
// Output:
// go 3
// rust 3
// c 1
Changing a map while ranging it
Deleting during a range is legal and is the normal way to evict expired cache entries. Inserting during a range is not something you can reason about, so build a key slice first and then insert.
package main
import "fmt"
func main() {
m := map[int]bool{1: true, 2: true, 3: true}
// Deleting is defined: an entry removed before it is reached
// will not be produced by this loop.
for k := range m {
if k == 1 {
delete(m, 2)
delete(m, 3)
}
}
fmt.Println(len(m))
// Adding is not defined: a new entry may be visited or skipped,
// so collect the keys first if you need to insert while walking.
}
// Output:
// 1
Discussion