1. Iterating over map and order
When you first see a map, you really want to treat it like a “table” where everything is laid out neatly and predictably. Especially if you previously worked with slices, where order is literally the point of the structure. But a map in Go is built differently: it’s optimized for fast key-based access, not for a “nice” order.
And this is where the most typical beginner bugs start: the program “seems to work,” and then suddenly it starts printing elements in a different order, tests become unstable, and a user asks, “why are tasks jumping around?”. The answer is: because map never promised you anything about the order, and that’s normal.
range over map: basic syntax
Go provides one universal way to iterate over collections: for range. It works for map too. And depending on what you need, you can get keys, values, or a pair at once.
Below is a small cheat-sheet table (it’s genuinely handy to keep in mind):
| What you want to get | How you write the loop | What ends up in the variables |
|---|---|---|
| Keys only | |
k — key |
| Values only | |
v — value |
| Both key and value | |
k — key, v — value |
A mini example, just to see “what it looks like”:
package main
import "fmt"
func main() {
ages := map[string]int{"alice": 30, "bob": 25}
for name, age := range ages {
fmt.Println(name, age) // line order is NOT guaranteed
}
}
Notice the comment: it’s not there for decoration, it’s there so that future “you” doesn’t set a trap for yourself two weeks later.
The main law of map: iteration order is not guaranteed
The most important idea in this lecture:
The iteration order over a map is not part of the contract.
This means that even if today you see the output:
alice 30
bob 25
tomorrow (or on a different computer, or after a small code change) you might see:
bob 25
alice 30
Why it works this way
A map is a structure where fast key lookup matters more than order. Internally, there’s hashing and various runtime optimizations, and Go also deliberately adds randomness to mechanisms related to map, including for security reasons and to protect against performance attacks.
Another important point: the map implementation really does change between Go versions—it’s optimized, sped up, and sometimes internal details change. The practical takeaway is simple: if you “accidentally” relied on ordering, updating Go can surprise you.
Example: “why does my program print differently than I expected”
In this lecture we continue our learning console app—a mini task tracker. For now it’s as simple as it gets: we store tasks in a map[int]string, where the key is id and the value is a short task title (one word, to keep input simple).
Let’s do a small piece: create tasks and print them using range.
package main
import "fmt"
func main() {
tasks := make(map[int]string)
tasks[1] = "read"
tasks[2] = "code"
tasks[3] = "sleep"
for id, title := range tasks {
fmt.Println(id, title) // order may change between runs
}
}
What matters here is not pretty output, but the fact: even if you added tasks “by increasing id,” you are not required to see 1, 2, 3 when printing.
And here’s the typical thinking mistake: “well, it always prints 1,2,3 for me—so the order must be fixed.” No. You just got lucky… so far.
About the “first element” of a map
A very common beginner question sounds like: “OK, the order isn’t guaranteed, but I can still do for k := range m { ...; break } and get some key. Why shouldn’t I do that?”
Technically, you’re right: you’ll get some key. But logically, you won’t get anything stable. Today it will be one key, tomorrow another. This breaks tests (you expect one thing, you get another), scripts (you parse output and it jumps around), and user experience (the same task list “dances”).
So the rule is simple: you cannot use the “first map element” as business logic. If you need “some random element,” then yes, that can be a valid idea. But it’s important to honestly call it a “random element,” not “the first.”
2. What range guarantees—and what it does not
So it doesn’t feel like “the ground disappeared under your feet,” let’s pin down what you can still treat as reliable when iterating a map, even though the order is undefined.
What is guaranteed is that on each iteration you get one pair (key and value) that actually exists in the map at the time of iteration. It’s also guaranteed that the range syntax is consistent and predictable: k has the key type K, and v has the value type V.
It is not guaranteed that the “first element” or the “last element” has any meaning. A map has no “first” and “last” as a concept. If you really want “the last added task,” that’s not a map problem—it means “you need a different data structure” (or additional data alongside it).
Nuance: changing the v variable does not change the value in the map
When you do:
for k, v := range m {
v = v + 1
}
many people expect the map to be updated. But v is a variable that receives a copy of the value from the map (for basic types like int, string, bool, this is especially obvious).
Let’s show a mini example:
package main
import "fmt"
func main() {
counts := map[string]int{"go": 1}
for _, v := range counts {
v++ // change only the local variable v
fmt.Println(v) // 2
}
fmt.Println(counts["go"]) // 1
}
If you really want to update a value in the map, do it through the key:
package main
import "fmt"
func main() {
counts := map[string]int{"go": 1}
for k, v := range counts {
counts[k] = v + 1
}
fmt.Println(counts["go"]) // 2
}
I’m showing this now on purpose, because logically you often do these things during iteration. But we still remember the “boundaries of the day”: we’re not turning map iteration into a “magic trick” of modifying the structure while iterating—we’re simply fixing the basic semantics: “the value in v is a copy.”
nil-map and range: safe
Good news: range over a nil-map is safe. It looks like this: there will simply be no iterations. That is, for range m executes the loop body zero times, and the program continues normally.
This is convenient in application code, because sometimes you receive a map as a parameter and you don’t want to write “if it’s nil, handle it separately.” Often you can just do range, and that’s it.
Mini example:
package main
import "fmt"
func main() {
var tasks map[int]string // nil
for id := range tasks {
fmt.Println(id) // will not run
}
fmt.Println("ok") // ok
}
This behavior fits well with the general zero values philosophy: zero values in Go often mean “do nothing, but safely.” (Though a reminder: you cannot write into a nil-map—that will cause a panic, but we already covered that in the previous lecture.)
3. When order is actually needed
Why “unstable order” matters in real programs
In practice, the ordering problem shows up most often in three places.
The first place is user output. You made a “task list” and print it. The user runs the program twice and sees different orders. They don’t think “oh, a hash table,” they think “it’s broken.” And honestly, they’re right in their own way: the UX really is broken.
The second place is tests and auto-checking. Even if you don’t write tests yet, you will run into them. Any check like “stdout must look exactly like this” will fail if the order jumps.
The third place is integrations and scripts. As soon as someone starts parsing your output (for example, grep, awk, or just another program), instability turns into unexpected bugs.
So what matters here is not the mere fact that “order isn’t guaranteed,” but discipline: don’t build logic on assumptions about order.
What to do if you do need an order
If you need an order, the correct idea sounds like this: create the order yourself. Usually that means: collect keys into a slice, sort the slice, then print using that slice. Note the key word here is “slice,” because slices are the things that can have order and can be sorted.
In newer versions of Go, the standard library keeps getting more convenient tools that help collect map keys into a slice and then work with them as an ordered sequence.
But the detailed “stable output recipe” is the next lecture of the day. For now, our task is to stop expecting order where there is none.
4. Typical mistakes when iterating over a map
Error #1: using range over a map as an “ordered list.”
Usually it looks like this: you print elements and assume that the “first printed” is the first added, or the smallest key, or something else “logical.” A map has no such contract. If you need order, create it separately (via keys and a slice).
Error #2: writing logic like “take the first element and exit the loop.”
Technically it works, but the result will be unstable. If you wanted a random element—fine, but then don’t call it “the first,” and don’t use it as “the main/default” without additional selection logic.
Error #3: changing the v variable and expecting it to change the map.
In a for k, v := range m loop, the v variable receives the value, but changing v does not change m[k]. If you want to update the value, assign it back via the key: m[k] = ....
Error #4: trying to “make it stable” just with nice formatting.
Sometimes a beginner thinks: “but I print with Printf, maybe the order will stabilize.” No: formatting affects what a line looks like, but not the iteration order.
Error #5: being surprised that the order changed after a small code change or a Go update.
That’s exactly a normal situation. The internal map implementation and mechanisms related to hashing, optimizations, and attack protection can change.
GO TO FULL VERSION