Maps
Maps are Go's built-in hash tables. They associate keys with values. Amortized O(1) lookup, insert, and delete. You'll use them constantly.
Learning objectives
- Create a map with
makeor a literal. - Use the comma-ok idiom to distinguish "missing" from "zero value".
- Delete keys and iterate in a deterministic order when needed.
- Avoid the nil-map write panic.
- Know what makes a valid key, and why slices can't be one.
Creating maps
// Literal, most common
m := map[string]int{
"Alice": 30,
"Bob": 25,
}
// With make
n := make(map[string]int)
// With capacity hint (for many inserts)
big := make(map[string]int, 1000)
Maps have a len but no cap.
Access, add, update
m["Charlie"] = 28 // add (or overwrite if key exists)
fmt.Println(m["Alice"]) // 30
If the key isn't in the map, Go returns the zero value of the value type. This is subtle and a common source of bugs:
fmt.Println(m["Nobody"]) // 0 , but that could also mean "0 years old"!
The comma-ok idiom
To tell "missing" from "zero":
age, ok := m["Alice"]
if ok {
fmt.Println("found:", age)
} else {
fmt.Println("missing")
}
This is one of the most-used idioms in Go. Internalize it.
Deleting keys
delete(m, "Bob")
delete is a built-in function. It's safe to call even if
the key isn't in the map, no-op.
Iteration (randomized!)
for k, v := range m {
fmt.Println(k, v)
}
keys := slices.Sorted(maps.Keys(m)) (Go 1.23+) or the
older sort.Strings(keys) pattern we saw in Chapter 10.
The nil-map trap
var m map[string]int // nil map
fmt.Println(m["x"]) // 0, READ is fine
m["x"] = 1 // PANIC: assignment to entry in nil map
You can read from a nil map but not write to it. Always create
with make or a literal before assigning.
What can be a key?
Any comparable type: basic types, pointers, arrays of comparable types, structs of comparable fields, interfaces.
Not allowed: slices, maps, functions. (They're not ==-comparable.)
Maps are reference types
a := map[string]int{"x": 1}
b := a
b["y"] = 2
fmt.Println(a) // map[x:1 y:2] , shared
Like slices, maps are effectively a handle to shared storage. Assigning doesn't copy. Pass them cheaply between functions.
Concurrent access & sync.Map
Maps are not safe for concurrent use. Two goroutines writing to the same map simultaneously causes a runtime panic (with the race detector) or data corruption (without).
Options for concurrent maps:
- Guard with a
sync.RWMutex(most common). sync.Map: a specialized concurrent map in the stdlib. Good for "append-only" or "many readers, few writers" patterns.- Actor pattern: one goroutine owns the map, others send requests via a channel.
Mutex pattern is covered in Chapter 24.
Check your understanding
Practice exercises
Word count
Write a function wordCount(s string) map[string]int that splits a string into whitespace-separated words and returns a count of each. Use strings.Fields.
Show solution
import "strings"
func wordCount(s string) map[string]int {
counts := make(map[string]int)
for _, w := range strings.Fields(s) {
counts[w]++ // zero value 0 makes this work for new keys
}
return counts
}
The key insight: counts[w]++ works for missing keys because the zero value is 0, then incremented to 1.
Stable ordered iteration
Given the word-count map from Exercise 1, print entries sorted by count (descending), then alphabetically on ties.
Show solution
import "sort"
type kv struct { Word string; Count int }
func printSorted(m map[string]int) {
kvs := make([]kv, 0, len(m))
for w, c := range m {
kvs = append(kvs, kv{w, c})
}
sort.Slice(kvs, func(i, j int) bool {
if kvs[i].Count != kvs[j].Count {
return kvs[i].Count > kvs[j].Count
}
return kvs[i].Word < kvs[j].Word
})
for _, e := range kvs {
fmt.Printf("%-10s %d\n", e.Word, e.Count)
}
}
Further reading
- The Go Blog, Go maps in action
sync.Mapreferencemapspackage: Go 1.21+ helpers (Clone, Copy, Keys, Values, Equal)
Hashes done.