Golang Map Iteration
In Go (Golang), iterating over maps is non-deterministic, meaning that the order in which you traverse the elements of a map is not guaranteed to be the same across iterations, even if the map itself has not changed. This characteristic can lead to unexpected behavior, especially when dealing with use cases like time series data or form data where the order of elements is significant.
I’ve also run into this in the context of unit testing, where I’ve had flaky tests that relied on iterating over a map of dependent testcases, or snapshotting the output of a function that iterated over a map.
Example
This example illustrates the non-deterministic nature of map iteration. The first iteration over the map is in the order that the elements were added, but the second iteration is in a different order.
package main
import (
"fmt"
)
func main() {
// Initialize a map
myMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
}
// First iteration
fmt.Println("First Iteration:")
for key, value := range myMap {
fmt.Printf("%s: %d\n", key, value)
}
fmt.Println("\nSecond Iteration:")
// Second iteration
for key, value := range myMap {
fmt.Printf("%s: %d\n", key, value)
}
}
Solutions
Use Slices for Ordered Data
Here, we maintain a separate slice of keys to preserve the order.
package main
import (
"fmt"
)
func main() {
myMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
}
keys := []string{"one", "two", "three", "four", "five"}
for _, k := range keys {
fmt.Printf("%s: %d\n", k, myMap[k])
}
}
Sort Keys
In this solution, we extract the keys, sort them, and then iterate over the sorted keys.
package main
import (
"fmt"
"sort"
)
func main() {
myMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
}
var keys []string
for k := range myMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s: %d\n", k, myMap[k])
}
}
Custom Data Structure
Here we use a custom struct to maintain both a map for quick access and a slice for order.
package main
import (
"fmt"
)
type OrderedMap struct {
Map map[string]int
Order []string
}
func main() {
omap := OrderedMap{
Map: map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
"five": 5,
},
Order: []string{"one", "two", "three", "four", "five"},
}
for _, k := range omap.Order {
fmt.Printf("%s: %d\n", k, omap.Map[k])
}
}
References
From the Go Language Specification:
For a map, the iteration order is not specified and is not guaranteed to be the same from one iteration to the next. If map entries that have not yet been reached are removed during iteration, the corresponding iteration values will not be produced. If map entries are inserted during iteration, the behavior is implementation-dependent, but the iteration values for each entry will be produced at most once.