Why Map Iteration Order in Go Is Random on Purpose
Go randomizes where a map loop starts so nobody can depend on its order. See the experiment, why small maps show only a few orders, and how to sort keys.

Run the same Go program twice and loop over the same map, and the entries can come out in a different order the second time. For people arriving from Python, where dictionaries remember insertion order, or from JavaScript, it looks like a bug. It is a deliberate decision, it has been there since the language began, and the practical consequence is a short list of bugs that only appear sometimes. This article shows the randomization with measurements, explains why the designers chose it, and gives you the pattern that makes output predictable. It sits right after the main guide to maps in the Go basics path.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
What the language promises
The Go specification is brief on this: the iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. Two further rules are stated in the same paragraph. If an entry that has not yet been reached is removed during iteration, it will not be produced. If an entry is created during iteration, it may be produced or skipped, and the choice can differ between entries and between iterations.
That is the whole contract. A conforming implementation could iterate in sorted order, in insertion order or in a fixed hash order, and your program would be correct as long as it never depended on which. The Go team chose a fourth option, which is to make the order random on purpose. The Go blog's article on maps gives the reason in one sentence: programmers had been relying on the stable order of the earlier implementation, so the runtime randomizes it.
Measuring it
Here is an experiment. A map has three entries. I loop over it ten thousand times, record the order of the keys each time, and count the distinct orders:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func order(m map[string]int) string {
var keys []string
for k := range m {
keys = append(keys, k)
}
return strings.Join(keys, "")
}
func main() {
m := map[string]int{"a": 1, "b": 2, "c": 3}
for range 6 {
fmt.Print(order(m), " ")
}
fmt.Println()
seen := map[string]int{}
for range 10000 {
seen[order(m)]++
}
fmt.Println(len(seen))
for _, k := range slices.Sorted(maps.Keys(seen)) {
fmt.Println(k, seen[k])
}
twenty := map[int]bool{}
for i := range 20 {
twenty[i] = true
}
first := map[int]int{}
for range 10000 {
for k := range twenty {
first[k]++
break
}
}
fmt.Println(len(first), "different first keys out of 20")
}abc bca cab abc bca abc
3
abc 7426
bca 1319
cab 1255
20 different first keys out of 20Six consecutive loops printed a mix of orders, and the count shows exactly three different ones across ten thousand runs. If the order were uniformly random there would be six, one for each permutation of three keys. The skew is instructive. One order, abc, came out about 75 percent of the time, and the other two shared the rest. A small map stores its entries in one group of eight slots. The runtime picks a random starting slot from the eight and walks around the group from there. Three entries sit in slots zero to two, so five of the eight starting points, plus slot zero itself, give abc, and one each gives bca and cab.
I would not build anything on that detail, because it describes one implementation (the map that arrived in Go 1.24) and not the language. What it does tell you is how dangerous small samples are. A unit test that iterates a tiny map and compares a string passes roughly three times in four even when it is wrong, which is exactly the odds that let a bug slip through review and then fail in CI one run in four.
For a larger map the starting point is also random, and the odds spread out. With twenty entries and ten thousand loops, the first key visited took on all twenty different values. A real map is larger than eight entries, so the production behavior is closer to "any order" than to "three rotations".
The bugs this causes
Randomization turns an assumption into a flaky failure instead of a quiet wrong answer. The ones I see most:
A test that compares output built from a map. The expected string is a snapshot of one run. It fails on a different run and nobody changed any code. Fix the code to sort, or compare as sets.
An API whose array order changes. A handler builds []Item by ranging over a map and returns it as JSON. Clients see items jump around between refreshes. The consistent fix is to sort by a field the client cares about, such as an ID or a name, before serializing.
A report or a log line that differs between runs. Fine for debugging and bad for anything diffed or cached, such as a generated config file whose content changes on every build and invalidates a cache layer.
Selecting "the first" entry. for k := range m { return k } returns an arbitrary key, a different one each run. Sometimes that is acceptable, as when you want any element, and in Go it is the simplest way to pick one. If you need the same one every time, sort.
A hash or checksum over a map. Feeding entries into a hash in iteration order gives a different digest per run. Sort first, or hash each entry and combine with an order-independent operation.
How to get a stable order
Collect the keys, sort them and loop over the slice. Since Go 1.23 that is a single expression:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
stock := map[string]int{"pears": 4, "apples": 12, "cherries": 0, "bananas": 7}
for _, name := range slices.Sorted(maps.Keys(stock)) {
fmt.Printf("%-10s %d\n", name, stock[name])
}
byCount := slices.SortedFunc(maps.Keys(stock), func(a, b string) int {
return stock[b] - stock[a]
})
fmt.Println(strings.Join(byCount, ", "))
fmt.Println(stock)
}apples 12
bananas 7
cherries 0
pears 4
apples, bananas, pears, cherries
map[apples:12 bananas:7 cherries:0 pears:4]slices.Sorted takes an iterator of ordered values and returns a sorted slice, and maps.Keys supplies the iterator. For a custom order, slices.SortedFunc takes a comparison function, as in the sort by stock level above. Before Go 1.23 you wrote it in three steps, collect into a slice, sort.Strings it and loop, and that version still works everywhere. The last line prints the map directly. fmt sorts map keys when it formats a map, so fmt.Println(stock) always shows map[apples:12 bananas:7 cherries:0 pears:4]. The same is true of encoding/json, which writes object keys in sorted order. The printing is stable and the iteration is not, which is why a quick Println during debugging can make you think the loop is ordered.
If you need the insertion order itself, the map cannot give it to you. Keep a slice of keys beside the map and append to it when a key is first added, or define a small type that owns both. An ordered map type is a few dozen lines, and more than one library provides it. The cost is the bookkeeping on delete, which needs to remove the key from the slice as well.
Modifying a map while ranging over it
The spec's rules about deletion and insertion are not just footnotes. Here is a loop that adds an entry for every entry it visits:
package main
import "fmt"
func main() {
m := map[int]int{}
for i := range 5 {
m[i] = i
}
visits := 0
for k := range m {
if k < 5 {
m[k+100] = 1
}
visits++
}
fmt.Println(len(m), visits)
}10 8The map ends with ten entries, as it must, since five were added. The count of visits is the part that varies: each of the five new entries may or may not be reached by the loop, so visits can be anywhere from five to ten, and it differs between runs. Deleting is the safe mutation. The idiom for k := range m { if bad(k) { delete(m, k) } } is explicitly allowed and is the right way to prune a map in place. Inserting during iteration is legal and unpredictable, so collect the new entries in a second map or slice and merge them after the loop.
Where randomness is the wrong thing to fight
Do not try to defeat the randomization with a fixed seed or a trick of insertion order. The order depends on the implementation and has changed across releases, and your program would need to be re-verified at each upgrade. Do not sort inside a hot loop either. If the order matters and the map is large, keep a sorted slice of keys and update it incrementally, or choose a data structure that is ordered by construction, such as a sorted slice searched with slices.BinarySearch, or a tree from a library.
And do not add sorting where order is irrelevant. Summing the values of a map, counting entries that match a condition or checking whether any entry satisfies a predicate gives the same answer in any order, and sorting those costs time for nothing. The sensible rule is the one the language gives you: treat the order as unspecified in your design, and sort in exactly the places where a person or a diff will see the result. The next step for the data structure is checking whether a key exists, and the maps package documentation lists the iterator helpers used here.


