AlgoMaster Logo

Iterating Maps

Last Updated: May 17, 2026

9 min read

Walking through every key-value pair in a map is a daily task: building reports, summing totals, filtering entries, exporting data. Go does this with the same range keyword used for slices, but with one twist that catches everyone off guard the first time. The iteration order is randomized on purpose, and your code has to be ready for that.

The range Loop

The range keyword over a map yields two values on each iteration: the key and the value. The shape mirrors slice iteration, but the meaning of the first value is different. For slices it's the index; for maps it's the key.

The order you see depends on the run. We'll get to why in a moment. For now, focus on the shape: for key, value := range m. Each iteration assigns one key-value pair to the two loop variables.

If you only need the keys, drop the second variable. The single-variable form yields keys, just like ranging over a slice yields indices.

This trips up people coming from other languages where a single-variable loop over a dictionary yields values. In Go, it's always keys. If you want values only, use the blank identifier for the key.

The total is the same regardless of iteration order, so this code is well-behaved even though range visits entries in an unpredictable sequence. That's a good general rule: if the answer doesn't depend on order, range alone is fine.

There's also a third form, for range m {}, with no loop variables at all. It runs the loop body once per entry without giving you the key or value. It's rarely useful, but it does come up when you only want to count entries (though len(m) is faster and clearer) or trigger a side effect for each entry without inspecting it.

Prefer len(prices) in production code; the empty range form is a curiosity worth recognizing but almost never the right tool.

Iteration Order is Random

Run the same loop twice and you'll likely see different orders. This is not a bug. The Go runtime deliberately randomizes the starting position of map iteration, and it does so on every range loop.

Same map, same program, two different orders. Run it again and you'll get yet another permutation. The point isn't that the order is reverse or scrambled in some specific pattern; it's that the order is unspecified and varies. Your code must not assume any particular sequence.

Why does Go do this? Map iteration order falls out of the internal hash table layout. If Go gave you a stable order, programmers would start depending on it. Then, the day Go changes the hash function or the bucket layout (for performance reasons, which has happened), every program that relied on the old order would break in subtle ways. Randomizing the order from day one prevents that whole class of bugs. The takeaway is "don't depend on order."

The same logic applies to test output. If you fmt.Println(m) directly, Go sorts the output by key as a convenience for readable test failures, but range does not. A test that compares iteration output as a string and happens to pass once may fail on the next run.

What's wrong with this code?

The code grabs the first key from a range loop and calls it the "cheapest item." There's no relationship between iteration order and price. The first key is whatever range happens to visit first, which is random. To actually find the cheapest, you have to scan every entry and track the minimum.

Fix:

Now the result depends on values, not on iteration order. The first flag handles the case where any starting value beats an uninitialized zero. We'll see this pattern again in the aggregation section.

The diagram shows the lifecycle of a single range loop. Go picks a random starting bucket, walks the table, and visits each entry once. Each new range starts at a fresh random position.

Mutation During Iteration

You can delete entries from a map while ranging over it, and Go specifies how that behaves. You can also add entries during iteration, but the behavior there is murkier. Read the rules carefully.

The Go specification says that during iteration:

  • Entries that have not yet been visited and get deleted during the loop will not be visited.
  • Entries that have not yet been visited and get inserted during the loop may or may not be visited.
  • The current entry (the one bound to the loop variables this iteration) is safe to delete; the loop already has it.

Deletion is the well-defined case. Insertion during iteration is officially unspecified, so don't rely on whether the new entry gets visited or not.

fmt.Println on a map sorts keys when printing, so the printed output above is deterministic even though the loop order isn't. The webcam entry is gone, and the others are untouched. This is the textbook safe pattern: walk the map, delete entries that fail a condition, move on.

What you must not do is assume a particular iteration order, then delete based on that assumption. The order is random, so "delete every other entry" or "delete the first three" doesn't make sense.

Adding entries during iteration is a different story. The behavior is unspecified, meaning Go reserves the right to visit the new entry, skip it, or anything in between. In practice you'll usually see one of those outcomes, but writing code that depends on either is a bug waiting to happen.

The output may or may not include c, and which it is can change between Go versions, between runs, or even between maps. If you need to act on entries that aren't in the map yet, collect them in a separate slice or map and merge after the loop ends.

Sorted Iteration Pattern

When you do need a specific order (alphabetical, numeric, insertion order from elsewhere), the idiom is the same: collect the keys into a slice, sort the slice, then range over the slice and look up values from the map.

Now the report is in alphabetical order on every run. The cost is one allocation (the keys slice) and one sort.

Pre-sizing the slice with make([]string, 0, len(revenue)) is a small but worthwhile habit: it tells append the final size, so no reslices happen inside the loop.

slices.Sort was added in Go 1.21 and works on any ordered type (numbers, strings, anything that supports <). Before 1.21, you'd write sort.Strings(keys) or sort.Ints(keys) from the sort package. Both still work today.

If you need values sorted instead of keys, the pattern changes slightly: collect key-value pairs into a slice of structs and sort by value.

The pattern is the same shape: range to collect, sort, range again to use. The only difference is what's in the slice and how the comparison function is written.

Go 1.21 also added a maps package with maps.Keys and maps.Values helpers that can shorten the collection step. For now, the manual collect-and-sort idiom works on every Go version and is clear about what it's doing.

Common Patterns: Aggregation

Maps are natural fits for aggregation: counting, summing, grouping. The range loop is the workhorse here.

Sum all values:

Sum is order-independent, so plain range works. No keys needed; the blank identifier skips them.

Find the maximum value:

The starting value of maxViews is -1, which is safe because view counts are non-negative. For a map of values that can be negative, initialize on the first iteration with a first flag, as we did earlier with the cheapest-item example.

The tie-break behavior matters in real systems. If "most-viewed" has two pages with 4500 views each, you'll get different answers on different runs. To get a stable tie-break, sort first or compare keys when values are equal.

Count occurrences:

counts[status]++ reads the current value (zero if missing), adds one, and writes back. This works because the zero value for an int in a map lookup is 0, so unknown keys start counting from zero automatically.

The diagram shows the flow of building a count map from a slice. Each element either bumps an existing counter or starts a new one. The ++ operator handles both cases in a single line because Go's zero-value lookup makes the missing-key branch invisible.

Common Patterns: Filtering and Reporting

A frequent need is "show me only the entries that match some condition," often in a specific order.

Filter into a new map:

make(map[string]int, len(stock)) hints at the size to avoid map growth during the loop. The hint isn't a hard limit, just a starting capacity. If half the entries pass the filter, we wasted some headroom; if all of them pass, we saved a couple of grows.

Sorted report combining filter and sort:

This is the bread-and-butter pattern for any "show me a report" task: filter while collecting keys, sort the keys, iterate to print. The output is now deterministic, sorted, and excludes zero-stock items.

Average value:

The empty-map check matters because dividing by zero is undefined for floats (it produces +Inf or NaN, not a panic, but the result is meaningless). Always guard aggregations that divide by len(m).

Putting It Together

Here's a small program that uses several of the patterns together: it builds a count map from raw events, filters out infrequent items, sorts the result, and prints a tidy report.

The pipeline is three range loops, each doing one thing. Count, then filter, then collect-and-sort. None of them depend on iteration order, because the only loop whose output reaches the user is the one over the sorted keys slice. That's the takeaway: keep order-sensitive output behind a sorted-keys step, and let everything else iterate however the runtime pleases.

Summary

  • for k, v := range m yields each key-value pair once. Use for k := range m for keys only and for _, v := range m for values only.
  • The empty form for range m {} runs the loop body once per entry without binding variables. Prefer len(m) when you just want a count.
  • Iteration order is randomized on every range loop. The Go runtime deliberately scrambles the starting position to prevent programs from depending on order.
  • Deleting an entry during iteration is well-defined: unvisited deleted entries will not be visited, and the current entry is safe to delete.
  • Inserting during iteration is unspecified. The new entry may or may not be visited, and you should not rely on either.
  • For sorted output, collect keys into a slice, sort with slices.Sort (Go 1.21+) or sort.Strings, then range the slice and look up values from the map.
  • Order-independent aggregations (sum, count, max, average) work fine with plain range. Operations that depend on order (first, "next N") need an explicit sort or other ordering step first.
  • Pre-size collection slices with make([]T, 0, len(m)) to avoid reslices inside the loop.

The next lesson cracks open the map's internal structure, the hash table and buckets that explain why iteration starts at random positions and why lookups are O(1) on average. Once you've seen the layout, the randomization rule stops feeling arbitrary and starts feeling inevitable.