24.9 maps Package (Go 1.21+): Keys, Values, Clone, Equal
Right, let’s talk about the maps package. You’ve been writing Go for a while, and you’ve undoubtedly written the same three map-related functions a dozen times: one to get a slice of the keys, another for the values, and a third to check if two maps are deeply equal. It’s a rite of passage, like building your own IKEA furniture with a butter knife. It works, but you always feel a little silly doing it when a proper toolkit is available.
Well, stop feeling silly. As of Go 1.21, the standard library finally includes the maps package, which is essentially the community saying, “We’ve all suffered enough, here are the utils.” It’s a small package, but it eliminates a ton of boilerplate. Let’s break it down.
Getting Keys and Values (and the Chaos Within)
The maps.Keys and maps.Values functions are the stars of the show. They do exactly what you think: return a slice of a map’s keys or values.
package main
import (
"fmt"
"maps"
)
func main() {
myMap := map[string]int{
"one": 1,
"two": 2,
"three": 3,
}
keys := maps.Keys(myMap) // ["one", "two", "three"] (order not guaranteed!)
values := maps.Values(myMap) // [1, 2, 3] (order corresponding to keys, which is also not guaranteed!)
fmt.Println("Keys:", keys)
fmt.Println("Values:", values)
}
Now, here’s the critical part you must engrave on your monitor: the returned slices are in random order. This isn’t a limitation of the maps package; it’s a fundamental property of Go maps. They are unordered collections. The package doesn’t magically impose order; it just efficiently copies whatever random order the runtime gives it. If you need sorted output, you still have to sort it yourself with slices.Sort—another fantastic new package we’ll get to.
Why is this the signature feature of a map? Security. If maps were always iterated in a deterministic order (like insertion order), a clever attacker could potentially engineer a denial-of-service attack by creating keys that cause terrible hash collisions, making your program crawl. By randomizing the iteration order, that attack vector is neutralized. It’s a trade-off: we sacrifice a predictable order for safer performance.
Cloning a Map (Without the Surprises)
maps.Clone returns a shallow copy of the input map. This is one of those functions that’s trivial to write but easy to get subtly wrong, so having it in the standard library is a win for consistency and correctness.
original := map[string][]byte{
"file1": []byte("hello"),
"file2": []byte("world"),
}
clone := maps.Clone(original)
// Changing the slice in the clone affects the original!
clone["file1"][0] = 'j'
fmt.Println(string(original["file1"])) // Prints "jello"
See that? The clone is a new map, but the values inside it are the same references. This is a shallow copy. If your map values are pointers, slices, or other maps, mutating those values will affect both the original and the clone. This is the behavior you’d expect, but it’s a classic footgun. If you need a deep copy, you’re still on the hook to implement that yourself, recursively.
Checking for Equality (Finally)
Before Go 1.21, comparing two maps was a nightmare. You couldn’t use ==, and writing a correct map[T]T equality function involved about six lines of checking lengths and iterating. maps.Equal fixes that.
mapA := map[rune]int{'a': 1, 'b': 2}
mapB := map[rune]int{'b': 2, 'a': 1}
equal := maps.Equal(mapA, mapB)
fmt.Println(equal) // Prints "true"
It correctly checks that both maps have the same set of keys and that the corresponding values are equal. And yes, it uses == for the values, which means it works for maps with comparable value types (like int, string, etc.) but will fail to compile if the values are themselves non-comparable types (like slices or other maps).
For that, we have its more powerful sibling: maps.EqualFunc. This allows you to provide your own comparison function for the values.
// Let's say we have maps of IDs to byte slices. We can't use == on slices.
map1 := map[int][]byte{1: {'a'}, 2: {'b'}}
map2 := map[int][]byte{1: {'a'}, 2: {'b'}}
// Use bytes.Equal to compare the values
equal := maps.EqualFunc(map1, map2, func(a, b []byte) bool {
return bytes.Equal(a, b)
})
fmt.Println(equal) // Prints "true"
This is incredibly useful for maps with complex values, letting you define what “equal” means in your specific context.
The One That Got Away: DeleteWhile
You might be wondering about a function like maps.DeleteFunc (which does exist). It iterates over the map and deletes every key-value pair for which the given function returns true. It’s straightforward and efficient.
scores := map[string]int{
"Alice": 85,
"Bob": 42,
"Eve": 100,
"Steve": 50,
}
// Delete everyone who scored below 60
maps.DeleteFunc(scores, func(key string, value int) bool {
return value < 60
})
// scores is now map[Alice:85 Eve:100]
The important thing to remember here is that you’re modifying the map during iteration, which is normally a big no-no that can cause panics. The maps package handles this safely for you, which is a great reason to use it instead of rolling your own loop.
So there you have it. The maps package is a testament to the Go philosophy: identify the most common, tedious, and error-prone patterns and build simple, robust tools to solve them. It doesn’t try to be clever. It just does the boring stuff correctly so you can focus on the more interesting problems.