Why Go’s Maps Are Unordered on Purpose
Randomness as a language design choice
“If your program depends on map iteration order, the bug already exists. Go just makes sure you notice.”
Iterating over a map in Go can be unsettling. Run the same loop twice and the output may change. For developers used to languages where map iteration feels stable, this behavior looks like unnecessary unpredictability.
It isn’t.
Go’s maps are unordered by design, and the runtime goes out of its way to make that fact impossible to ignore. This choice is one of the clearest expressions of Go’s philosophy: do not allow incorrect assumptions to feel correct.
Maps are not sequences
At the language level, a Go map is not a list, an array, or a set with ordering semantics. It is a lookup table optimized for fast access by key. There is no “first” key, no “next” key, and no meaningful traversal order.
Yet developers often assume otherwise because stable iteration order is comforting.
Consider this simple example:
m := map[string]int{
"a": 1,
"b": 2,
"c": 3,
}
for k, v := range m {
fmt.Println(k, v)
}Run this code multiple times and the output order may change. The map has not changed. Your assumptions have.
The danger of accidental correctness
If map iteration order were stable, code like the example above would quietly acquire meaning it was never meant to have. Tests would pass. Output would look consistent. Over time, developers would begin to rely on that order, even though the language never promised it.
Hyrum’s Law (The Law of Implicit Dependencies). The Go developers follow a principle akin to “Hyrum’s Law” (if a behavior is observable, someone will depend on it, even if it’s an accident of the current implementation). In early versions (pre-Go 1) of Go, map iteration order was stable, and programmers began relying on it.
Hash tables have no natural order
Under the hood, Go maps are hash tables. Their layout depends on hash values, bucket placement, resizing behavior, and memory layout. None of these factors produce an order that is meaningful or stable.
Even small changes can reshuffle iteration order:
m["d"] = 4Adding a single element may cause the map to grow and rehash internally, changing the order of every key. Any appearance of consistency is accidental, and Go treats it as such.
Maps in Go are implemented as hash tables, which are designed for efficient key-value lookups, insertions, and deletions with an average time complexity of O(1). Maintaining a specific order (like insertion or key-sorted order) would require additional overhead in memory and CPU cycles. By not guaranteeing order, Go’s runtime implementers have the freedom to change the underlying hash table algorithm in future versions (e.g., switching to Swiss tables) without breaking existing code that might depend on a specific ordering.
Randomness as a correctness tool
One of the most insidious states in software engineering is deterministic incorrectness: code that is technically wrong but appears correct due to environmental stability. “Stable-but-undefined” behavior creates a dangerous illusion of reliability, allowing latent bugs to survive in production until an unrelated runtime change triggers a catastrophic failure.
To prevent the ossification of undefined behavior, the Go runtime explicitly destabilizes map iteration. Unlike a linear scan starting at index zero, the iterator initializes with a randomized start bucket and offset for every range loop. The runtime ensures that any logic predicated on accidental ordering fails loudly and immediately during development.
This design forces implicit dependencies into the open, converting subtle data corruption bugs into hard crashes. Ultimately, this approach creates a binary outcome: code is either robustly order-independent, or it is visibly broken.
𝐋𝐞𝐚𝐫𝐧 𝐭𝐨 𝐛𝐮𝐢𝐥𝐝 𝐆𝐢𝐭, 𝐃𝐨𝐜𝐤𝐞𝐫, 𝐑𝐞𝐝𝐢𝐬, 𝐇𝐓𝐓𝐏 𝐬𝐞𝐫𝐯𝐞𝐫𝐬, 𝐚𝐧𝐝 𝐜𝐨𝐦𝐩𝐢𝐥𝐞𝐫𝐬, 𝐟𝐫𝐨𝐦 𝐬𝐜𝐫𝐚𝐭𝐜𝐡. Get 40% OFF CodeCrafters: https://app.codecrafters.io/join?via=the-coding-gopher
When order matters, be explicit
Go does not prevent ordered iteration. It requires you to say so clearly.
The idiomatic pattern is to extract keys, sort them, and then iterate:
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, m[k])
}This extra work is intentional. It makes the cost and meaning of ordering visible. Anyone reading the code can immediately tell that order matters here—and that it was not an accident.
Using a slice
package main
import (
"fmt"
"sort"
)
func main() {
m := make(map[int]string)
m[1] = "a"
m[2] = "c"
m[0] = "b"
var keys []int
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
}For testing and printing purposes, the fmt package automatically prints maps in key-sorted order for convenience, but this does not affect general program iteration.
Runtime freedom and future-proofing
By explicitly leaving map iteration order undefined, Go decouples the language specification from the runtime implementation. This abstraction barrier grants the compiler and runtime team the flexibility to optimize internals without violating the user contract.
It permits the evolution of hashing algorithms to mitigate collisions (e.g., HashDoS attacks) and allows for radical restructuring of bucket layouts to enhance cache locality. Consequently, code that assumes a deterministic order is relying on an implementation detail rather than the spec—a classic example of Hyrum’s Law. This enforcement of non-determinism is a safeguard for long-term language maintainability.
Security is part of the picture
Predictability in systems software is often synonymous with vulnerability. Go treats map iteration not just as a logic concern, but as a side-channel.
Go employs randomized hashing (specifically targeting the seed) to mitigate HashDoS (Algorithmic Complexity Attacks). Without this, attackers could engineer a set of keys that collide into a single bucket, degrading O(1) lookup performance to O(N), causing CPU exhaustion.
However, randomized hashing alone is insufficient if the iteration order remains deterministic based on those hash values. A stable iteration order leaks information about the bucket distribution and the underlying hash seed, potentially allowing an attacker to reverse-engineer the seed via differential analysis of output orders. By explicitly randomizing the iteration offset, Go masks the internal memory layout. In this context, determinism is an information leak; randomness is a necessary layer of defense.
What this reveals about Go
Go consistently favors runtime enforcement over social conventions. This philosophy is baked into the language’s memory model and standard library design:
Nil Maps. Writing to a nil map triggers a panic immediately (protecting against null pointer dereferences in the runtime logic), whereas reading returns a zero value (safety).
Mutex Copying. Copying a
sync.Mutexinvalidates its internal atomic counters and semaphore references, which are address-dependent. Go’svettool statically analyzes code to forbid this via thenocopymechanism.Map Iteration. The runtime actively injects entropy to prevent “implementation coupling.”
The language refuses to trust that developers will read documentation footnotes. Instead, it builds active guardrails directly into the runtime environment. Go assumes that if an API can be misused, it will be misused, and therefore the system must actively prevent the comfortable but incorrect usage patterns.
Final thought
Go’s unordered maps are a strict adherence to semantic correctness. In type theory, a Map (or Associative Array) implies a distinct lack of ordering; it is a set of mappings, not a sequence.
If order is required, the data structure must reflect that intent (e.g., a Slice or a Tree). By refusing to provide accidental ordering guarantees, Go forces the code to align with the theoretical definition of the data structure. It turns a subtle semantic mismatch—treating a Set like a Sequence—into an immediately visible runtime inconsistency. Order is a constraint; if you need it, you must explicitly program for it.





Interestingly enough, maps (dicts) in Python are insertion-ordered. While this is a side effect of the current implementation, this behavior was useful enough that it was made standard. Somehow that doesn't apply to sets, though.
I must be solving a different category of problems in Go, though, because somehow I haven't yet need this behavior. I have one Java application where this is necessary.
Thank you for this! My code was breaking during an assignment for a Distributed Systems class. it took me a couple of hours to realize that it is by design that Go iterates randomly. I truly held an implicit assumption about map ordering!