Iterating Maps
How to loop over key-value pairs in Go maps using range, handle random iteration order, and produce deterministic ordered traversal when needed.
Basic Iteration with for range
Go’s for range loop reads every entry in a map exactly once. The syntax hands you both the key and the value on each turn.
scores := map[string]int{
"alice": 92,
"bob": 85,
"carol": 88,
}
for name, score := range scores {
fmt.Printf("%s scored %d\n", name, score)
}
The loop visits every pair in scores and prints them. The variables name and score are local to each iteration; you can name them whatever you like.
If you only need the keys, drop the second variable:
for name := range scores {
fmt.Println("Player:", name)
}
If you only need the values, discard the key with the blank identifier _:
total := 0
for _, score := range scores {
total += score
}
fmt.Println("Total points:", total)
A for range over a nil map runs without panicking; the loop body simply never executes. That means you can safely write code that iterates over a map that might be nil, as long as you don't try to write to it.
var nilMap map[string]int
for k, v := range nilMap {
fmt.Println(k, v) // never runs
}
Nil Maps and Writes:
Reading from or iterating over a nil map is fine, but assigning a value to a nil map causes a runtime panic. Before you populate a map inside a loop, initialize it with make or a literal — even if you plan to iterate over it first.
Why Iteration Order Is Unpredictable
If you run the first example multiple times, the output order will very likely change between runs — and even between two consecutive loops on the same map inside a single program execution.
Go intentionally randomises the starting point of every map iteration. This is not a bug; it is a deliberate design choice that has been in place since the early releases and was hardened further in Go 1.12 so that every for range loop over a map begins at an unpredictable position inside the hash table.
The mechanism: Go maps are hash tables. Each key is fed to a hash function, and the resulting number determines which “bucket” the entry lands in. The iteration cursor walks through buckets in memory order, not insertion order or sorted order. The runtime picks a random bucket to start from, so the sequence observed by your loop changes every time.
This randomness serves two concrete purposes:
- It prevents accidental reliance on order. If iteration were stable, developers would start depending on that order without realising it. The moment the runtime or the hash function changed, code would break silently in production. Randomness exposes those bugs during development.
- It keeps the door open for internal optimisations. The Go team can change the hash table implementation without worrying about code that tied itself to a specific iteration pattern.
Never Assume an Iteration Order:
Any program that expects a map to yield keys in a particular sequence is broken. Relying on order is the single most common mistake newcomers make with map iteration. Even if the order looks consistent on your machine today, it will change on a different system, after a Go upgrade, or simply when the map grows.
The one guarantee you do have: when the map is not modified during the loop, every entry present at the start of iteration will be visited exactly once. No entries are skipped, and no phantom entries appear.
What Happens If You Modify the Map While Iterating
Go does not forbid adding or deleting entries inside a for range loop over a map, but the behaviour is undefined. The language specification deliberately leaves it unspecified which new entries will be seen by the loop, and which deleted entries will still be visited.
m := map[int]string{1: "a", 2: "b", 3: "c"}
for k, v := range m {
fmt.Println(k, v)
if k == 2 {
m[4] = "d" // adding a new key
delete(m, 1) // removing an existing key
}
}
In practice, the newly added key 4 might appear later in the same loop, or it might not. The deleted key 1 might already have been visited, or it might be skipped entirely. There is no way to predict the outcome, and it varies across Go versions.
If you need to modify a map based on a condition discovered during iteration, use a two‑pass approach: collect the modifications in a separate structure first, then apply them after the loop finishes.
m := map[string]bool{"alpha": true, "beta": false, "gamma": true}
toDelete := []string{}
for k, v := range m {
if !v {
toDelete = append(toDelete, k)
}
}
for _, k := range toDelete {
delete(m, k)
}
This keeps the iteration pass unchanged and avoids the undefined territory altogether.
Do Not Modify a Map During Iteration:
Adding or removing entries while a for range is active leads to behaviour that is not defined by the language specification. The loop may miss entries, see entries multiple times, or panic in rare cases. Always separate discovery from mutation.
Producing Deterministic Iteration Order
When you need a predictable sequence — for a sorted report, a reproducible log, or a stable test assertion — you must impose the order yourself. The map itself will not provide it.
Sorted by Key
Extract all keys into a slice, sort the slice, and then look up each key in turn.
import "sort"
inventory := map[string]int{"pencil": 45, "eraser": 20, "notebook": 12}
keys := make([]string, 0, len(inventory))
for k := range inventory {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, inventory[k])
}
Pre‑allocating the keys slice to len(inventory) avoids re‑allocations as it grows, but the make with capacity alone and append is the idiomatic way — clear and correct.
For integer keys, use sort.Ints. For floating‑point, sort.Float64s. For custom types, implement sort.Interface or use sort.Slice.
Sorted by Value
Sort the same key slice, but compare the values associated with those keys.
keys := make([]string, 0, len(inventory))
for k := range inventory {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool {
if inventory[keys[i]] == inventory[keys[j]] {
return keys[i] < keys[j] // tie-break by key
}
return inventory[keys[i]] < inventory[keys[j]]
})
for _, k := range keys {
fmt.Printf("%s: %d\n", k, inventory[k])
}
The tie‑breaker on keys makes the sort stable and reproducible even when values are equal.
Insertion‑Order Iteration
A plain Go map does not remember the order in which entries were added. If you need to replay entries in insertion order, you must maintain a separate data structure that records the sequence.
A common pattern combines a slice for ordering and a map for fast lookups:
type OrderedMap struct {
keys []string
values map[string]int
}
func NewOrderedMap() *OrderedMap {
return &OrderedMap{values: make(map[string]int)}
}
func (om *OrderedMap) Set(key string, value int) {
if _, exists := om.values[key]; !exists {
om.keys = append(om.keys, key)
}
om.values[key] = value
}
func (om *OrderedMap) Range(fn func(key string, value int)) {
for _, k := range om.keys {
fn(k, om.values[k])
}
}
This works well when insertions dominate. Deletion from the slice is O(n); if you need heavy deletions in insertion order, a doubly linked list (container/list) can reduce deletion to O(1) at the cost of higher memory overhead and more complex code.
Choosing the Right Ordering Strategy:
- Sorted by key: best for human‑readable output and deterministic testing, at the cost of O(n log n) per ordered traversal.
- Sorted by value: when the display order depends on computed metrics.
- Insertion order: when the sequence of addition carries meaning (event logs, configuration files, FIFO processing). Accept the extra memory and complexity only when you truly need insertion order.
Using the maps Package Iterators (Go 1.23+)
Since Go 1.23, the standard library maps package provides maps.Keys and maps.Values functions. Unlike the earlier experimental versions that returned slices, these functions return iterators — lightweight objects you can use directly in a for range loop without materialising an entire slice in memory.
import (
"fmt"
"maps"
)
m := map[string]int{"alpha": 1, "beta": 2, "gamma": 3}
for k := range maps.Keys(m) {
fmt.Println(k)
}
for v := range maps.Values(m) {
fmt.Println(v)
}
The iterator approach is memory‑efficient for large maps because it does not allocate a separate slice. If you genuinely need a slice of keys (for sorting, for example), you can collect the iterator with slices.Collect:
import (
"maps"
"slices"
"sort"
)
m := map[string]int{"zulu": 1, "alpha": 2, "tango": 3}
keys := slices.Collect(maps.Keys(m))
sort.Strings(keys)
When You See Iterator-Based Code:
If you encounter maps.Keys(m) in modern Go code and it compiles, you’re on Go 1.23 or later. The function returns an iterator, not a slice. Wrap it with slices.Collect to obtain a slice when you need one. This is the intended migration path from the old x/exp/maps API.
Performance Perspective
Iterating over a map with for range visits each bucket in the internal hash table, probing slots until every entry has been seen. The time taken grows linearly with the number of entries, O(n). The random starting point has no measurable impact on total iteration cost.
When you sort keys before iterating, the sort step adds O(n log n) overhead. For maps with only a few hundred entries this is usually fine, but for very large datasets in performance‑sensitive code, consider whether you can work with the unsorted iteration or maintain a separate ordered structure that avoids repeated sorting.
Pre‑allocating the key slice to the map’s size (make([]KeyType, 0, len(m))) avoids multiple slice growths, but the benefit is modest for small maps. Clarity trumps micro‑optimisation until a profiler tells you otherwise.
Common Mistakes Specific to Iteration
Assuming insertion order. A for range over a map does not visit entries in the order they were added. It never has, and it never will. Code that relies on this assumption will fail unpredictably.
Depending on a “consistent” order observed during local testing. The fact that you ran the same program three times and saw the same order does not mean the order is stable. Different machines, different Go versions, or even a different size of the same map can change it.
Modifying the map inside the loop body. Adding or deleting keys inside the same loop that iterates the map is undefined. The outcome is not guaranteed and can cause missed entries, double visits, or panics. Defer modifications until after the loop.
Using range on a nil map without checking its nil status for writes. Iteration is safe on a nil map, but if the loop body attempts to write to that map, the program panics. The loop should either initialise the map ahead of time or guard against writes when nil.
Expecting maps.Keys to return a slice in Go 1.23+. That function returns an iterator, not a slice. If you pass it to code that expects a slice, it will not compile. Use slices.Collect to get a slice when required.
When you understand that Go intentionally withholds a fixed iteration order, the rest of map iteration becomes simple: use for range for everyday traversal, and build a separate slice when you need sorting or insertion order. The maps package iterators bring a memory‑aware alternative for Go 1.23 onward.