Order matters (and maps don't keep it)

Station 2.5 11 min Interchange: insertion-ordered arrays & ksort() → map iteration order

Here's a PHP guarantee you've probably never thought about: an array remembers the order you put things in. foreach walks it in that order, json_encode writes it in that order, and every report you've ever built has quietly relied on it.

Go maps make no such promise. range over a map visits the keys in some order, and Go goes further: it randomises the starting point every time you range. Two loops over the same map, one after the other in the same program, can disagree.

Run 1Oval Bank Angel Moorgate
Run 2Oval Bank Angel Moorgate
Run 3Moorgate Oval Bank Angel
Your testpasses on your laptop, fails in CI
Go randomises map order so you find out on run 3, not in production.

That's deliberate. Early Go maps happened to iterate in a fairly stable order, code started depending on it, and it broke whenever the implementation changed. So the Go team made the order unpredictable, on purpose, to stop anyone relying on it.

Sorting the keys

When you want a stable order, usually alphabetical, sort the keys and range over those:

for _, s := range slices.Sorted(maps.Keys(taps)) {
	fmt.Printf("%s: %d\n", s, taps[s])
}

maps.Keys(taps) hands out the keys one at a time (it's an iterator, which you can range over), and slices.Sorted collects them into a new, sorted slice. You then look each value up as you go. It's PHP's ksort followed by a foreach, except the map itself is never changed.

Sorting by value, such as busiest station first, needs a custom comparison. That's slices.SortFunc, in the next lesson.

Keeping insertion order

Sometimes the order isn't alphabetical at all. A train calls at Bank, then Moorgate, then Angel, and sorting would scramble that. When order matters, keep it in a slice, next to the map:

route := []string{"Bank", "Moorgate", "Angel"}
due := map[string]int{
	"Bank":     2,
	"Moorgate": 4,
	"Angel":    7,
}

for _, stop := range route {
	fmt.Println(stop, due[stop])
}

The slice remembers the order, and the map does the fast lookups. Together they're what a PHP array gave you for free. (When each entry has several fields, a slice of structs is often tidier still. That's lesson 2.7.)

Mind the gap

Printing a map looks sorted, and that fools people. fmt.Println(taps) prints map[Angel:1 Bank:3 Oval:2], with the keys in order, every time. That's fmt sorting them to make output readable. It says nothing about range, which is still random. encoding/json sorts map keys too (7.3). So a quick print looks fine, while the loop in your report still shuffles.

Recap

  • Ranging over a map visits keys in an unpredictable order, randomised on purpose, even between two loops in one run.
  • For alphabetical order: for _, k := range slices.Sorted(maps.Keys(m)), then look up m[k].
  • For insertion order, keep the keys in a slice and use the map for lookups.
  • fmt and encoding/json sort map keys when printing. range doesn't.

1Iteration order

PHPorder.php
<?php
$taps = ['Oval' => 2, 'Bank' => 3, 'Angel' => 1];

foreach ($taps as $station => $n) {
    echo "{$station} ";
}
// Oval Bank Angel, every time
GOorder.go
taps := map[string]int{"Oval": 2, "Bank": 3, "Angel": 1}

for station := range taps {
	fmt.Print(station, " ")
}
// any order, and it can change between loops

Mind the gap

PHP arrays remember insertion order. Go maps keep no order at all, and Go shuffles the starting point on purpose, so no code can come to rely on it.

2Sorted by key

PHPsorted.php
<?php
ksort($taps); // sorts the array itself, by key

foreach ($taps as $station => $n) {
    echo "{$station}: {$n}\n";
}
GOsorted.go
for _, station := range slices.Sorted(maps.Keys(taps)) {
	fmt.Printf("%s: %d\n", station, taps[station])
}

Mind the gap

maps.Keys hands out the keys, and slices.Sorted collects them into a sorted slice. Then you range over that slice and look each value up.

3Keeping insertion order

PHPinsertion.php
<?php
$due = [];
$due['Bank'] = 2;
$due['Moorgate'] = 4;
$due['Angel'] = 7;
// iterates in the order added
GOinsertion.go
route := []string{"Bank", "Moorgate", "Angel"}
due := map[string]int{"Bank": 2, "Moorgate": 4, "Angel": 7}

for _, stop := range route { // the slice keeps the order
	fmt.Println(stop, due[stop])
}

Mind the gap

When order matters, keep it in a slice. The map does the lookups, and the slice remembers the order.