CHAPTER 14 · COMPOSITE TYPES

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 make or 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)
}
!
Map order is intentionally randomized Go shuffles iteration order so you can't accidentally depend on it. Need determinism? Collect keys, sort, iterate: 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

EXERCISE 1

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.

EXERCISE 2

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

Hashes done.