Maps

A map stores values under keys; lookups of missing keys return the zero value, and writing to a nil map panics.

Syntaxm := 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

Example · go
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.

Example · go
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.

Example · go
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.

Example · go
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

  • Be the first to comment on this lesson.