Maps
A map stores values under keys; lookups of missing keys return the zero value, and writing to a nil map panics.
m := make(map[K]V) // m[k] = v, v, ok := m[k], delete(m, k), len(m)A map stores values under keys and finds a value by its key instead of by position. map[string]int is a map whose keys are strings and whose values are ints. Keys are unique: storing under an existing key replaces the old value.
The four operations
m["a"] = 1 stores. v := m["a"] reads. delete(m, "a") removes, and deleting a key that is not there does nothing at all. len(m) counts the entries.
Missing keys and comma-ok
Reading a key that is not in the map is not an error. It gives the zero value of the value type: 0 for an int, "" for a string, false for a bool. That is convenient and it hides one question: was the key absent, or was a zero stored under it? The second form of a map read answers it. v, ok := m["a"] sets ok to true only when the key was present. This is called the comma-ok form, and v := m["a"] is shorthand for it with the ok thrown away.
Creating one
make(map[string]int) gives an empty, writable map, and map[string]int{} is shorthand for the same thing. A map literal with entries is map[string]int{"a": 1}. Key types must be comparable, so strings, numbers, bools and arrays work; slices and maps cannot be keys.
What trips people up
The zero value of a map is nil, and a nil map has no storage. Reading from it is fine and returns zero values. Writing to it panics with "assignment to entry in nil map". A map declared with var m map[string]int and never passed through make is nil.
Example
package main
import "fmt"
func main() {
// A map literal with two entries.
ages := map[string]int{"ada": 36, "alan": 41}
ages["grace"] = 45 // adds a new key
ages["ada"] = 37 // replaces the value under an existing key
fmt.Println(len(ages), ages["ada"])
// A missing key reads as the value type's zero value, not an error.
fmt.Println(ages["nobody"])
// The comma-ok form tells a missing key apart from a stored zero.
age, ok := ages["alan"]
fmt.Println(age, ok)
_, ok = ages["nobody"]
fmt.Println(ok)
// Deleting a key that is not there does nothing.
delete(ages, "alan")
delete(ages, "nobody")
fmt.Println(len(ages))
// make gives an empty writable map. A nil map would panic here.
counts := make(map[string]int)
counts["hits"]++
fmt.Println(counts)
}
// Output:
// 3 37
// 0
// 41 true
// false
// 2
// map[hits:1]
When to use it
- Count how many requests each route handled.
- Look up a configuration value by its name.
- Group a list of database rows by one of their fields.
- Check membership in an allow list with a single lookup.
More examples
Counting with the zero value
Counters need no initialisation step because a missing key already reads as 0. This is the shape of nearly every tally in Go: request counts per route, errors per code, words per document.
package main
import "fmt"
func main() {
words := []string{"go", "run", "go", "build", "go"}
counts := map[string]int{}
for _, w := range words {
// On the first sight of w this reads 0 and stores 1.
// Longhand for the same step: counts[w] = counts[w] + 1
counts[w]++
}
fmt.Println(counts["go"], counts["run"], counts["missing"])
fmt.Println(len(counts))
}
// Output:
// 3 1 0
// 3
Grouping into a map of slices
Grouping rows by a field is the most common map-of-slices job: orders per customer, log lines per request id. Note that you must assign the append result back into the map, because the map holds a copy of the slice.
package main
import "fmt"
func main() {
words := []string{"ant", "bee", "ape", "bat"}
// The value type is a slice. A missing key reads as a nil slice,
// and append works on a nil slice, so no setup is needed.
byLetter := map[string][]string{}
for _, w := range words {
k := w[:1]
byLetter[k] = append(byLetter[k], w)
}
// The keys are known here, so print them in a fixed order.
for _, k := range []string{"a", "b"} {
fmt.Println(k, byLetter[k])
}
}
// Output:
// a [ant ape]
// b [bee bat]
A map used as a set
A map[T]bool is the usual stand-in for a set of values. Membership tests become a single lookup, and the zero value false means an absent key answers correctly without any extra check.
package main
import "fmt"
func main() {
// Only the keys carry meaning; the bool value is always true.
allowed := map[string]bool{"GET": true, "POST": true}
for _, method := range []string{"GET", "DELETE"} {
// A missing key reads as false, so no comma-ok is needed.
if allowed[method] {
fmt.Println(method, "allowed")
} else {
fmt.Println(method, "rejected")
}
}
}
// Output:
// GET allowed
// DELETE rejected
Discussion