skip to content

Why does ranging over a Go map yield a different order each run, and how do you produce stable output?

level: juniorimportance: must knowfreq 78%

answer

  1. the spec promises nothing here
  2. and the runtime makes sure you notice
  3. a random start, chosen per loop
  4. even two entries flip between runs
  5. keys into a slice, sort, then index

basics

~10 s

Go deliberately randomises map iteration, so no order is guaranteed or repeatable. For stable output, copy the keys into a slice, sort that slice, then read the map in that key order.

solid answer

~50 s

The Go specification says the iteration order over a map is unspecified, and the runtime goes further: every `for k := range m` statement starts at a randomly chosen position, so the same map can iterate differently on two consecutive loops in the same process. That is a deliberate design choice — it stops code from quietly growing a dependency on an order the implementation is free to change. There is no ordered map in the standard library and no flag to turn the randomisation off. When output must be stable — rendering a report, writing a config file, hashing contents, asserting in a test — collect the keys into a slice, sort the slice, and index the map in that order. If you need insertion order specifically, keep a separate slice of keys alongside the map and append to it on first insert.

code

go · 8 lines
go
keys := make([]string, 0, len(counts))
for k := range counts {
	keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
	fmt.Println(k, counts[k])
}

go deeper

for a junior

Be ready to say plainly that Go map iteration order is unspecified and randomised, and to write the four-line fix: make a key slice, range to fill it, sort it, then loop over the slice indexing the map.

for a middle

Explain that the randomisation is per range statement rather than per map, and that it is a deliberate defence against code growing a dependency on an order the implementation may change.

for a senior

Show where this actually costs you in production — churning diffs in generated files, fingerprints and cache keys that never match, and tests that flake only after a fixture grows a second entry.

for a principal

Frame it as an API rule for your codebase: a map is a lookup structure, and anything whose order is observable to a caller, a file or a hash should be carried in a slice so ordering is a decision someone made rather than an accident.

## What the language actually promises The Go specification is blunt: the iteration order over a map is not specified and is not guaranteed to be the same from one iteration to the next. That is a *promise of nothing*, not an accident of the implementation. The runtime backs the promise up. Each time a `for k := range m` statement begins, it picks a random position in the map's storage and walks from there, wrapping around. Two consequences follow that surprise people: 1. **It is not once per map.** Two range loops over the same untouched map, in the same run of the same binary, may produce two different orders. 2. **Small maps are not exempt.** A two-entry map flips roughly half the time. A test that iterates a one-entry map and asserts the output will pass forever, then start failing the day someone adds a second entry — the classic "it worked until the fixture grew" flake. ## Why randomise at all Because an unspecified order that happens to be stable is worse than a random one. If the order were merely arbitrary but repeatable, code and tests would accumulate dependencies on it, and then a future change to the map implementation — a different growth strategy, a different hash seed — would break programs that never knew they were relying on anything. Randomising makes the violation show up on the developer's machine, early and loudly, instead of in production two releases later. Go also seeds the map hash randomly per process, which is why the order changes between runs and not only between loops. ## The deterministic pattern The idiom is always the same shape: **maps are for lookup, slices are for order.** Extract the keys, impose an order on the slice, then drive the map from the slice. ```go keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } slices.Sort(keys) for _, k := range keys { use(k, m[k]) } ``` The `make` with a capacity of `len(m)` avoids regrowing the slice, and `slices.Sort` gives the total order. For non-string keys, sort on whatever field defines your intended presentation order, and make sure that order is *total* — if two keys can compare equal, the tie is still nondeterministic. ## Where the requirement actually bites - **Anything written to a file or a wire format.** Generated code, rendered configuration, CSV exports: unsorted iteration produces a file whose diff churns on every run even when nothing changed. - **Anything hashed or signed.** An ETag, a cache key or a content fingerprint computed by walking a map is a different value every time, which silently defeats caching. - **Tests.** Any assertion on a concatenated or listed result must sort first, or compare with an order-insensitive check. - **Logs you compare by eye.** Two log lines for the same state that read differently waste debugging time. ## The thing that hides the problem Printing a map with the `fmt` package shows the keys in sorted order, and `encoding/json` also sorts map keys when marshalling. So a developer printf-debugging a map sees perfectly stable, sorted output and concludes the map itself is ordered. It is not: those packages sort on your behalf as they print. The moment code ranges over the map directly, the randomisation is back. ## What Go does not give you There is no `LinkedHashMap` equivalent in the standard library, no comparator you can attach to a map, and no build flag or environment variable that pins iteration order. If insertion order matters, you must record it yourself: keep a `[]K` of keys in insertion order beside the `map[K]V`, or store a slice of key/value structs and use a map only as an index into it. If sorted-by-key access matters constantly, keep the keys in a sorted slice and use binary search, or a tree structure you maintain yourself. ## What an interviewer is listening for That you say "unspecified, and deliberately randomised" rather than "it just happens to vary"; that you know it varies per loop and not only per process; that your fix is keys-into-a-slice-then-sort rather than something that pretends to sort the map; and that you recognise the categories of code — output, hashing, tests — where the requirement is real rather than sorting everything reflexively.

  • Does a Go map with only two entries iterate in a stable order?
    No. The randomisation applies whatever the size, so a two-entry map comes out in either order roughly half the time. Size only changes how often a flaky assertion happens to pass — a one-entry map always passes, which is exactly how a test survives until someone adds a second fixture.
  • If I need the entries back in insertion order, what do I use?
    You maintain it yourself: keep a slice of keys alongside the map and append a key the first time it is inserted, or hold a slice of key/value structs and use the map only as an index into that slice. The standard library has no ordered map type.
  • Is there a build flag or environment setting that turns map-order randomisation off?
    No, and that is intentional. There is no supported way to pin the order, because pinning it would let code depend on an order the implementation is explicitly free to change. If you need order, impose it in your own code by sorting.

A map is a bag, not a queue. If you want the items in an order, you tip the bag out onto the table and arrange them yourself.

saying these in an interview costs you the question

  • Says a Go map iterates in insertion order
  • Claims the order is at least stable within one process run
  • Believes small maps iterate deterministically
  • Talks about sorting the map itself rather than a key slice
  • Concludes order is fine because fmt printed it sorted
  • Says a build flag or GODEBUG setting can pin the order