for-range over Maps — Specification¶
Source: Go Language Specification — §For_range (map iteration)
1. Spec Reference¶
Map iteration via for-range is governed by the Go Language Specification at:
https://go.dev/ref/spec#For_range
The specification explicitly and deliberately leaves map iteration order unspecified. The relevant spec text states:
"The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next. If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced. If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next. If the map is nil, the number of iterations is 0."
This is one of the most important behavioral guarantees in the Go spec — the deliberate randomization of map iteration is a language-level contract, not an implementation detail.
2. Formal Grammar¶
ForStmt = "for" [ Condition | ForClause | RangeClause ] Block .
RangeClause = [ ExpressionList "=" | IdentifierList ":=" ] "range" Expression .
For map iteration specifically:
// All valid forms for map[K]V
for k, v := range m { } // key and value (both new variables)
for k, v = range m { } // key and value (both existing variables)
for k := range m { } // key only
for k = range m { } // key only (existing variable)
for range m { } // neither (count or side-effects only)
for _, v := range m { } // value only (discard key)
The range expression m must be of type map[K]V. The first iteration variable receives type K; the second receives type V.
3. Core Rules & Constraints¶
- Iteration order is never guaranteed. The spec explicitly states it is not specified and not consistent between runs.
- Randomization is intentional. The Go runtime has randomized map iteration since Go 1.0, specifically to prevent programs from accidentally depending on a particular order.
- Nil map range is safe. Ranging over a nil map produces zero iterations — no panic.
- Deleting during iteration is safe. Keys removed before being visited will not appear in subsequent iterations.
- Adding during iteration is indeterminate. New keys added during iteration may or may not be visited — this is explicitly left unspecified.
- Concurrent access is a data race. Reading and writing a map from multiple goroutines without synchronization is undefined behavior, detected by the Go race detector.
- Range evaluates the map expression once. If you reassign the variable holding the map, iteration continues over the original map.
- Value is a copy.
vinfor k, v := range mis a copy ofm[k]. Modifyingvdoes not change the map. - Key is a copy.
kis also a copy of the key — for pointer or reference types, modifying the pointed-to value throughkwould affect the map's associated key, but reassigningkitself does not.
4. Type Rules¶
Map type constraints for range¶
The map type map[K]V requires: - K must be a comparable type (can be used as map key) - V can be any type
Valid key types: bool, int (and variants), float32, float64, complex64, complex128, string, pointers, channel types, interface types, array types (if element is comparable), struct types (if all fields are comparable).
Invalid key types: slice, map, func (not comparable).
Iteration variable types¶
var m map[string][]int
for k, v := range m {
// k is string
// v is []int (a copy of the slice header — same underlying array)
}
Important: when V is a slice, map, or pointer, v is a shallow copy. Modifying v's elements modifies the original data.
5. Behavioral Specification¶
Randomized iteration order¶
The Go runtime randomizes map iteration by: 1. Choosing a random starting bucket. 2. Choosing a random starting offset within that bucket. 3. Traversing remaining buckets in a wrapping order from that point.
This means:
m := map[string]int{"a": 1, "b": 2, "c": 3}
// Run 1 might print: a b c
// Run 2 might print: c a b
// Run 3 might print: b c a
// All are valid and expected
for k := range m {
fmt.Println(k)
}
The randomization is seeded per map iteration start. Even within a single program run, two consecutive range loops over the same map may produce different orders.
Deletion during iteration¶
Per the spec: "if a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced."
m := map[int]string{1: "a", 2: "b", 3: "c", 4: "d"}
for k := range m {
if k%2 == 0 {
delete(m, k) // safe to delete current or future keys
}
}
// Result: only odd keys remain (but we may have visited even keys before deleting them)
The current key being iterated is always safe to delete. Future unvisited keys will not appear if deleted.
Addition during iteration¶
Per the spec: "If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped."
m := map[int]int{1: 1}
for k, v := range m {
if k < 5 {
m[k+1] = v + 1 // new keys may or may not be visited
}
}
// Final map could be {1:1, 2:2} or {1:1, 2:2, 3:3, ...} depending on runtime
// Do NOT rely on specific behavior here
Nil map¶
var m map[string]int // nil map
for k, v := range m {
// this block never executes
_ = k
_ = v
}
// No panic, zero iterations
Concurrent map access¶
// THIS IS UNSAFE — data race:
m := make(map[string]int)
go func() {
for range m { } // read
}()
go func() {
m["x"] = 1 // write
}()
// The Go runtime detects concurrent map read/write and panics:
// "concurrent map read and map write"
// or "concurrent map iteration and map write"
Use sync.RWMutex or sync.Map for concurrent map access.
6. Defined vs Undefined Behavior¶
| Situation | Behavior |
|---|---|
| Range over nil map | Defined — zero iterations, no panic |
| Range over empty map | Defined — zero iterations |
| Delete current key during range | Defined — safe |
| Delete not-yet-visited key during range | Defined — that key is not produced |
| Delete already-visited key during range | Defined — has no effect on iteration |
| Add new key during range | Indeterminate — may or may not be visited |
| Concurrent write while ranging | Undefined — data race, runtime panic |
| Concurrent read while ranging | Defined (reads are safe without writes) |
Modify value v from range | Defined — only affects local copy |
Modify map value via m[k] = ... during range | Defined — new value visible to subsequent code |
Reassign map variable m = otherMap during range | Defined — range continues over original map |
7. Edge Cases from Spec¶
Shallow copy of value¶
type Node struct{ Val int }
m := map[string]*Node{"a": {Val: 1}}
for k, v := range m {
v.Val = 99 // modifies the Node that m["a"] points to
_ = k
}
fmt.Println(m["a"].Val) // 99 — pointer copy still points to same Node
But:
for k, v := range m {
v = &Node{Val: 999} // only reassigns local v, does NOT change m[k]
_ = k
}
fmt.Println(m["a"].Val) // still 99
Deterministic order requires explicit sorting¶
import "sort"
m := map[string]int{"banana": 2, "apple": 5, "cherry": 3}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, m[k])
}
// apple: 5
// banana: 2
// cherry: 3
Map cleared during iteration¶
m := map[int]int{1: 1, 2: 2, 3: 3}
visited := 0
for k := range m {
visited++
if visited == 1 {
// clear all entries after visiting the first key
for key := range m {
delete(m, key)
}
}
}
fmt.Println("visited:", visited) // 1 — remaining keys were deleted before being visited
Nested range over same map¶
m := map[int]int{1: 10, 2: 20, 3: 30}
for k1 := range m {
for k2 := range m {
// Two independent iterations — each has its own random start
// k1 and k2 may produce different orderings
_ = k1
_ = k2
}
}
sync.Map for concurrent use¶
import "sync"
var sm sync.Map
// Safe concurrent write
sm.Store("key", 42)
// Safe concurrent range
sm.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // return false to stop iteration
})
8. Version History¶
| Version | Change |
|---|---|
| Go 1.0 | for-range over maps introduced with randomized order |
| Go 1.0 | Randomization intentionally added to prevent order-dependent code |
| Go 1.6 | Runtime detects concurrent map read/write and panics (rather than silent corruption) |
| Go 1.9 | sync.Map added to standard library for concurrent-safe map |
| Go 1.21 | clear(m) builtin added — removes all keys from a map |
9. Implementation-Specific Behavior¶
How map iteration randomization works internally¶
Go maps are implemented as hash tables with buckets. Each bucket holds up to 8 key-value pairs. The runtime's map iteration:
- At the start of each
for range, callsmapiterinitwhich picks a random starting bucket usingfastrand(). - Also picks a random starting cell within that bucket.
- Iterates through all buckets wrapping around.
This internal mechanism is not part of the spec and can change between Go versions.
Map growth during iteration¶
If the map grows (rehashes) during iteration, the runtime adjusts the iterator state to handle the new bucket layout. This is why adding keys during iteration is indeterminate — the iterator may or may not encounter the new bucket where the added key landed.
Memory layout¶
Go's map implementation uses a hmap struct internally. The for range uses a hiter struct to track iteration state. Both are unexported and subject to change.
10. Spec Compliance Checklist¶
- Map iteration order is never assumed to be stable or sorted
- Sorted iteration uses explicit key extraction +
sortpackage - Concurrent map access uses
sync.RWMutexorsync.Map - Nil maps are ranged over only when the nil case is intentional (zero iterations)
- New keys are not added during range when the program must visit them
- Value copy semantics are understood —
vdoes not modify the map -
clear(m)(Go 1.21+) is used instead of manual delete loops when appropriate -
sync.Map.Rangeis used for concurrent-safe map iteration - Race detector (
go test -race) is used to catch concurrent map access bugs
11. Official Examples¶
Basic map range¶
package main
import "fmt"
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 82,
"Carol": 91,
}
// Order is NOT deterministic
for name, score := range scores {
fmt.Printf("%s scored %d\n", name, score)
}
}
Keys only¶
package main
import "fmt"
func main() {
exists := map[string]bool{
"go": true,
"java": true,
"rust": true,
}
for lang := range exists {
fmt.Println(lang)
}
}
Safe deletion during range¶
package main
import "fmt"
func removeExpired(sessions map[string]int, cutoff int) {
for id, expiry := range sessions {
if expiry < cutoff {
delete(sessions, id) // safe: spec guarantees this is fine
}
}
}
func main() {
sessions := map[string]int{
"abc": 100,
"def": 200,
"ghi": 50,
}
removeExpired(sessions, 150)
fmt.Println(sessions) // map[def:200]
}
Deterministic sorted iteration¶
package main
import (
"fmt"
"sort"
)
func printSorted(m map[string]int) {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf(" %s: %d\n", k, m[k])
}
}
func main() {
inventory := map[string]int{
"apples": 10,
"bananas": 5,
"oranges": 8,
}
printSorted(inventory)
// apples: 10
// bananas: 5
// oranges: 8
}
Nil map range (safe)¶
package main
import "fmt"
func countItems(m map[string]int) int {
count := 0
for range m { // works even if m is nil
count++
}
return count
}
func main() {
fmt.Println(countItems(nil)) // 0
fmt.Println(countItems(map[string]int{"a": 1, "b": 2})) // 2
}
Concurrent-safe iteration with sync.Map¶
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
// Store values
m.Store("host", "localhost")
m.Store("port", 8080)
m.Store("debug", true)
// Concurrent-safe range
m.Range(func(key, value any) bool {
fmt.Printf("%v = %v\n", key, value)
return true // continue iteration
})
}
Counting with map range (avoiding value)¶
package main
import "fmt"
func main() {
wordCount := map[string]int{
"the": 50,
"go": 30,
"func": 20,
}
total := 0
for range wordCount {
total++
}
fmt.Println("unique words:", total) // 3
// Or more directly:
fmt.Println("unique words:", len(wordCount)) // 3
}
12. Related Spec Sections¶
| Section | URL |
|---|---|
| For range | https://go.dev/ref/spec#For_range |
| Map types | https://go.dev/ref/spec#Map_types |
| Map literals | https://go.dev/ref/spec#Composite_literals |
| Index expressions (map access) | https://go.dev/ref/spec#Index_expressions |
| Deletion of map elements | https://go.dev/ref/spec#Deletion_of_map_elements |
| Comparison operators (key constraints) | https://go.dev/ref/spec#Comparison_operators |
| sync.Map documentation | https://pkg.go.dev/sync#Map |
| Go blog: maps in action | https://go.dev/blog/maps |
| Go memory model (concurrent access) | https://go.dev/ref/mem |