A range over a Go map can visit the same entries in a different order on consecutive iterations. The language specification leaves map iteration order unspecified and gives no guarantee that a later pass over an unchanged map will repeat an earlier sequence.
That contract is stronger than saying that maps are merely unsorted. An unsorted container could still expose a stable insertion-dependent or storage-dependent sequence. A Go program cannot assign such meaning to map traversal.
Order is outside the map contract
For a map m, this loop produces every entry that qualifies under the language rules, but the sequence is not part of the program’s portable behavior:
for k, v := range m {
consume(k, v)
}Code remains correct when consume is insensitive to order. Problems appear when traversal order escapes into an observable result: serialized text, generated configuration, snapshot output, tie-breaking, cache keys, or a choice of the first encountered entry.
The distinction matters even when repeated runs appear stable on one machine. Observed consistency does not create a language guarantee.
The runtime actively varies traversal
Current Go runtime map code explicitly randomizes iteration by selecting randomized offsets when an iterator is initialized. This implementation behavior reinforces the language contract, but it is not the contract itself.
The runtime map implementation has also changed substantially over Go’s lifetime. Modern Go uses a Swiss Table based design with map data split across tables as capacity grows. Storage layout, growth strategy, and iterator internals are implementation details and can evolve without granting programs a stable traversal sequence.
As a result, code should not infer ordering from bucket placement, hash values, allocation history, or a particular runtime release.
Mutation during iteration has separate semantics
Unspecified order does not mean every aspect of map iteration is unspecified. The language defines behavior for entries added or removed while a loop is active.
If an entry that has not yet been reached is deleted, that entry is not produced. If a new entry is inserted during iteration, it may be produced or skipped. The decision can vary by entry and by iteration.
These rules permit useful in-loop mutation patterns while leaving sequence unconstrained. They also mean that a traversal over a mutating map cannot be treated as a snapshot of either the initial or final key set.
Concurrent mutation is a different boundary. Ordinary maps do not support unsynchronized concurrent reads and writes. Iteration combined with a concurrent writer requires synchronization around the shared map.
Stable output requires an explicit ordering layer
When output order is part of an interface, the ordering operation belongs outside the map. A common representation is a sorted key slice:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
emit(k, m[k])
}The sort makes the intended order visible in code and independent of map internals. For ordered key types, slices.Sorted(maps.Keys(m)) provides the same boundary with the iterator APIs available in recent Go releases.
This extra work has a cost. Collecting keys requires space proportional to the number of entries, and comparison sorting costs O(n log n). That cost is the price of imposing an order that the hash map itself does not provide.
Tests expose accidental order dependencies
Tests often reveal this boundary first. A function can be logically correct while a golden file or exact string assertion fails because map-backed data was emitted directly.
The durable fix is determined by the interface contract. If order is semantically relevant, production code should establish it before emitting the result. If order is irrelevant, the test should compare data without assigning significance to sequence.
Relying on one observed traversal sequence makes a test sensitive to runtime details, process state, and implementation changes. Re-running the loop is not a valid normalization mechanism because another iteration carries the same unspecified-order contract.
First-entry selection is nondeterministic policy
Selecting the first key encountered by range is compact but does not define a portable selection rule:
for k := range candidates {
return k
}The result can vary even when candidates contains the same entries. If the selected key affects routing, leader choice, shard assignment, fallback behavior, or persistent state, map iteration has silently become policy.
A deterministic policy needs an explicit criterion such as lexical order, numeric priority, timestamp, hash ranking with a fixed algorithm, or a separately maintained sequence. The map can still provide lookup efficiency while another structure carries ordering semantics.
Go maps provide key-based access without an iteration-order promise. Treating traversal sequence as disposable keeps runtime implementation freedom separate from application-visible determinism.