The bug that breaks on CI
You write a loop over a map. On your laptop, the keys print in alphabetical order. You commit the code. The CI server runs the test. The test fails. The keys came out in a different order. You stare at the screen. The code hasn't changed. The data hasn't changed. The universe just decided to shuffle your map.
This happens because Go map iteration order is random. The language specification explicitly requires the runtime to randomize the order of keys during iteration. This isn't a bug. It's a deliberate design choice. The Go team built this randomness to prevent code from relying on a specific order. If the compiler allowed a stable order, developers would write code that assumes the first key is the oldest. Then the runtime changes an internal detail, or you upgrade Go, and the code breaks silently. Randomization catches the bug immediately. The code fails fast, and you fix the logic.
Randomness by design
Go forces map iteration to be non-deterministic. The spec says the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. This rule applies to every map type. It applies to map[string]int, map[User]Profile, and nested maps. The runtime enforces this by picking a random starting point in the hash table every time you start a range loop.
Other languages handle this differently. Python dictionaries preserve insertion order since version 3.7. JavaScript objects have a defined property enumeration order. Go chose a different path. The Go team prioritized performance and simplicity. Preserving insertion order requires extra metadata or linked lists inside the map structure. That adds memory overhead and slows down map operations. Go maps are optimized for fast lookups and updates. Randomization costs almost nothing. It just picks a random bucket index at the start of the loop.
Maps are for lookup, not for order.
Minimal example
Here's the simplest map iteration. Run this program five times. The output changes every time. The compiler won't warn you. The code is valid. The randomness is the feature.
package main
import "fmt"
func main() {
// map literal creates the hash table with three entries
m := map[string]int{"a": 1, "b": 2, "c": 3}
// range picks a random start bucket for this iteration
for k, v := range m {
// output varies per run; never assume order here
fmt.Println(k, v)
}
}
The output might look like this on one run:
# output:
a 1
b 2
c 3
And like this on the next run:
# output:
c 3
a 1
b 2
The keys and values are the same. The order is different. Your code must handle both cases.
How the runtime shuffles your data
Under the hood, a Go map is a hash table. The runtime stores keys and values in buckets. Each bucket holds a fixed number of entries, usually eight. When you insert a key, the runtime hashes the key and finds the right bucket. If the bucket is full, the runtime chains to an overflow bucket. The hash table grows dynamically as you add more entries.
When you iterate with range, the runtime creates an iterator object. This object tracks the current bucket, the current index within the bucket, and the overflow chain. The critical detail is the starting point. The runtime picks a random bucket index at the beginning of the iteration. It doesn't start at bucket zero. It starts at a random bucket, walks through that bucket, follows the overflow chain, then jumps to the next bucket, and wraps around to the beginning.
The random seed changes every program run. This guarantees non-determinism. The iterator doesn't shuffle the keys. It just starts at a random place and walks the table linearly. This is efficient. The runtime doesn't need to copy keys or sort them. It just traverses the existing structure. The cost of randomization is a single random number generation at the start of the loop.
The hiter struct in the runtime holds the state. It contains the current bucket pointer, the overflow bucket pointer, the tophash array for probing, and the startBucket index. The next function advances the iterator. It checks the current bucket, moves to the next index, handles overflow, and wraps around. The random startBucket ensures the traversal order changes every time.
The JSON trap
The most common place this bites you is JSON marshaling. You marshal a struct containing a map. The JSON output has keys in a random order. You diff two JSON files. The diff shows changes everywhere, even though the data is identical. Your build pipeline rejects the commit. You need deterministic output.
The encoding/json package respects map iteration order. It iterates the map and writes the keys as it encounters them. Since the map iteration is random, the JSON output is random. This breaks tools that rely on stable diffs. Git diffs, CI checks, and configuration management tools all suffer from this.
Here's a realistic example. You have a config struct with a map of settings. You marshal it to JSON. The output varies.
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Name string
Options map[string]string
}
func main() {
cfg := Config{
Name: "app",
Options: map[string]string{
"debug": "true",
"port": "8080",
"host": "localhost",
},
}
// json.Marshal iterates the map in random order
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
panic(err)
}
// output varies per run; diffs will fail
fmt.Println(string(data))
}
The output might look like this:
# output:
{
"Name": "app",
"Options": {
"debug": "true",
"host": "localhost",
"port": "8080"
}
}
Or this:
# output:
{
"Name": "app",
"Options": {
"port": "8080",
"debug": "true",
"host": "localhost"
}
}
The data is identical. The order is different. Your diff tool sees a massive change. You need to fix this.
Fixing non-deterministic output
The solution is to sort the keys before iterating. You extract the keys into a slice, sort the slice, and then iterate the map using the sorted keys. This produces deterministic output. The sort package provides efficient sorting functions. sort.Strings sorts a slice of strings in lexicographical order.
Here's the pattern. Extract keys, sort them, iterate.
package main
import (
"encoding/json"
"fmt"
"sort"
)
type Config struct {
Name string
Options map[string]string
}
// MarshalJSON implements custom marshaling for deterministic output
func (c Config) MarshalJSON() ([]byte, error) {
// create a temporary struct to hold the sorted options
type Alias Config
return json.Marshal(&struct {
Options map[string]string `json:"Options"`
*Alias
}{
Options: c.Options,
Alias: (*Alias)(&c),
})
}
func main() {
cfg := Config{
Name: "app",
Options: map[string]string{
"debug": "true",
"port": "8080",
"host": "localhost",
},
}
// extract keys to a slice
keys := make([]string, 0, len(cfg.Options))
for k := range cfg.Options {
keys = append(keys, k)
}
// sort.Strings orders the keys alphabetically
sort.Strings(keys)
// build a new map with sorted keys for deterministic iteration
sortedOptions := make(map[string]string)
for _, k := range keys {
sortedOptions[k] = cfg.Options[k]
}
cfg.Options = sortedOptions
// now marshaling produces stable output
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
This approach works, but it's verbose. A better approach is to use a custom marshaler that builds the JSON manually, or to use a library that handles sorted maps. The key takeaway is that you must sort the keys if you need deterministic output. Don't rely on the map. Sort the keys. Diff the output. Sleep well.
Pitfalls and silent failures
The compiler won't save you here. There is no undefined error for assuming map order. The code compiles clean. The bug lives in your logic. You might write code that picks the "first" item from a map as a default value. That default changes every run. The worst map bug is the one that only fails on the server, not on your machine.
Another pitfall is testing. You write a test that compares the output of a function that returns a map. The test fails randomly. You add a retry loop. The test passes sometimes. This is a flaky test. Flaky tests erode trust in your test suite. You need to make tests deterministic. Sort the keys before comparing. Or compare the map contents directly, ignoring order.
The Go community accepts that maps are unordered. If you need order, you sort. The standard library provides sort.Strings for slices of strings. You extract keys, sort them, then iterate the map using the sorted keys. This pattern is idiomatic. Don't fight the map. Extract, sort, iterate.
Convention aside: The sort package is part of the standard library. You don't need third-party tools. sort.Strings, sort.Ints, and sort.Slice cover most use cases. Use sort.Slice when you need to sort by a custom key. The receiver name in your sort function should be descriptive. Don't use s or x. Use users or items. Clear names make the code readable.
Choosing the right structure
You need to pick the data structure that matches your access pattern. Maps are fast for lookups. Slices are fast for iteration. Sorted slices give you order. The decision depends on what you're doing.
Use a map when you need fast lookups and don't care about order. Use a slice when you need to preserve insertion order. Use a sorted slice of keys when you need deterministic iteration for output or comparison. Use the sort package to order keys before iterating a map. Use a linked list or a tree when you need frequent insertions and deletions in the middle of the sequence.
Pick the data structure that matches your access pattern.