How to Iterate Over a Map in Go (And Why Order Is Random)

Iterate over a Go map using a range loop, but expect random order as Go intentionally shuffles traversal to prevent reliance on sequence.

The map that lies to you

You write a loop to process a map of user scores. You print the results. The first time, Alice is first. The second time, Bob is first. The third time, Charlie. You stare at the screen. The code hasn't changed. The data hasn't changed. The order is jumping around like a pinball. You suspect a bug in your logic. You add a sort. It works. You remove the sort. It breaks again.

The map isn't broken. Your assumption is.

Go maps do not preserve insertion order. They do not sort by key. They do not sort by value. Every time you iterate over a map, the runtime returns the entries in a random order. This is not a bug. This is a deliberate design choice to stop you from writing code that depends on sequence. If you need order, you must sort explicitly. The map will never do it for you.

Hash maps and the randomness guarantee

Go maps are hash maps. A hash function turns a key into a large integer. That integer determines where the value lives in memory. The runtime scatters entries across buckets based on their hash. It never stores keys in the order you inserted them.

When you start a loop, the runtime picks a random starting bucket. It walks the buckets, yielding key-value pairs. The starting point changes every time. This prevents your code from accidentally relying on the internal layout. If the map implementation changes, or if the map grows and rehashes, your code keeps working because it never assumed an order.

Think of a post office with numbered mailboxes. You drop letters in based on the recipient's zip code hash. The mail carrier doesn't deliver in the order letters arrived. The carrier grabs a random mailbox number to start, then walks the row. If you relied on the carrier always starting at mailbox 1, your delivery schedule would collapse the moment the carrier decided to start at mailbox 42. Go forces you to sort if you need order. It stops you from writing brittle code.

Randomness also helps with load distribution. If you iterate over a map of backend servers to pick one for a request, randomization ensures you don't always hit the same server first. The first entry in a deterministic iteration would get hammered. Random start points spread the load evenly across the map.

Maps are for lookup. Slices are for order. Know the difference.

The iteration loop

Here's the standard pattern. Create a map, range over it, process the entries.

package main

import "fmt"

func main() {
    // Map literal creates the map and populates it.
    scores := map[string]int{
        "alice": 10,
        "bob":   20,
        "carol": 30,
    }

    // Range yields key and value in random order.
    for name, score := range scores {
        // Print each pair. Run this multiple times to see the order change.
        fmt.Printf("%s: %d\n", name, score)
    }
}

The range keyword works on maps. It returns two values: the key and the value. The loop variables are declared with := inside the for statement. This is the idiomatic way to iterate. If you only need the keys, you can drop the value variable entirely. The compiler optimizes away the value copy.

// Iterate keys only. The blank identifier is not needed here.
for name := range scores {
    fmt.Println(name)
}

Using for name := range scores is cleaner than for name, _ := range scores. The compiler recognizes the single-variable form and skips the value extraction. This follows the convention of writing the simplest code that expresses the intent.

What happens under the hood

When you compile, the compiler sees range m. It generates code to call the map iteration function. At runtime, the iteration function grabs the map header. It picks a random starting bucket index using a fast pseudo-random number generator. The generator is not cryptographically secure. It just needs to break order quickly. The cost is negligible.

The iterator walks the buckets. Each bucket holds up to eight key-value pairs. If more keys hash to the same bucket, an overflow bucket links to the next one. The iterator follows the chain. It yields pairs until all buckets are exhausted.

The map might resize while you iterate. If the map grows beyond a threshold, the runtime allocates new buckets and moves entries. The iterator detects this. It continues iterating over the old buckets and the new buckets to ensure you see every entry. This makes iteration safe even during growth, as long as no other goroutine is writing to the map.

If another goroutine writes to the map while you iterate, the runtime panics. The error is fatal error: concurrent map iteration and map write. This is a runtime check, not a compiler error. The iterator captures the map header. If the header changes, the runtime catches the mismatch and stops the program. This protects you from reading torn data or crashing with undefined behavior.

Maps are not thread-safe. Wrap the map in a struct with a sync.Mutex if multiple goroutines need access. Or use sync.Map for specific read-heavy workloads where keys are mostly stable.

Concurrent panics are the map equivalent of a segfault. Protect the map or isolate the access.

Real code needs deterministic order

Tests fail intermittently when they depend on map order. You build a slice from a map and compare it to a golden file. The test passes on your machine. It fails in CI because the random order differs. You add a sort to the test assertion. The test stabilizes.

Production code often needs order too. You might process configuration settings where dependencies matter. You might render a list of items for a user. You might generate a report where rows must be sorted. In these cases, collect the keys, sort them, and iterate the sorted list.

Here's how to get deterministic order. Collect keys, sort, iterate.

package main

import (
    "fmt"
    "sort"
)

// ProcessConfig reads settings and applies them in alphabetical order.
func ProcessConfig(config map[string]string) {
    // Collect keys to sort them before processing.
    // Pre-allocate capacity to avoid reallocations.
    keys := make([]string, 0, len(config))
    for k := range config {
        keys = append(keys, k)
    }

    // Sort keys to ensure deterministic processing order.
    sort.Strings(keys)

    // Iterate over sorted keys to access map values in order.
    for _, k := range keys {
        // Apply setting. Order matters for dependency resolution.
        fmt.Printf("Applying %s = %s\n", k, config[k])
    }
}

The slice keys holds the map keys. sort.Strings orders them lexicographically. The loop iterates the slice, which has a fixed order. The map lookup inside the loop is fast. This pattern is common in Go. It separates the data structure from the ordering requirement.

Pre-allocating the slice capacity with len(config) is a small optimization. It avoids reallocations as you append keys. The compiler can't always infer the final size, so hinting helps.

Sort the keys. Never trust the map to sort for you.

Pitfalls that trip up beginners

Range over a map yields copies of the values. If the value is a struct, you get a copy. Modifying the value in the loop does not update the map. You need the address of the value or update via the key.

package main

import "fmt"

type User struct {
    Name string
    Age  int
}

func main() {
    users := map[string]User{
        "alice": {"Alice", 30},
    }

    // Range yields a copy of the value.
    for k, u := range users {
        // Modifying u does not change the map entry.
        u.Age = 31
        fmt.Printf("Loop: %s is %d\n", k, u.Age)
    }

    // Map still holds the original value.
    fmt.Printf("Map: %s is %d\n", "alice", users["alice"].Age)
}

The output shows the loop variable changed, but the map did not. To update the map, use the key.

// Update via key to modify the map entry.
for k := range users {
    users[k].Age = 31
}

Or take the address if the value is a pointer.

// Map of pointers allows direct modification.
usersPtr := map[string]*User{
    "alice": {"Alice", 30},
}

for _, u := range usersPtr {
    // u is a pointer. Modifying u.Age updates the map entry.
    u.Age = 31
}

Maps of pointers are common when you need to mutate values during iteration. The pointer is cheap to copy. The underlying struct lives on the heap.

Another pitfall is checking for existence. If you access a key that doesn't exist, Go returns the zero value for the value type. You can't distinguish between "key exists with zero value" and "key missing". Use the two-value form of map access.

// Two-value form checks existence.
val, ok := m[key]
if !ok {
    // Key is missing.
}

The ok boolean is true if the key exists. It is false if the key is missing. This is the idiomatic way to check presence. Don't compare the value to zero. Zero might be a valid value.

Range gives copies. Update by key or take the address.

When to use maps and when to reach for something else

Go provides several data structures. Maps are powerful, but they aren't the right tool for every job. Pick the structure that matches your requirements.

Use range over a map when you need to visit every key-value pair and the order is irrelevant to the logic.

Use a sorted key slice when you require deterministic ordering for reproducible tests or stable output.

Use for k := range m when you only need the keys and want to avoid copying the values.

Use a slice of structs when you need to preserve insertion order or maintain a fixed sequence.

Use a mutex-protected map when multiple goroutines read and write the map concurrently.

Use sync.Map when you have high contention and many readers with few writers, though standard maps with mutexes often work fine and are easier to reason about.

Maps are for lookup. Slices are for order. Know the difference.

Where to go next