05 / 20 · Day 2
Day 2 · Concept 05

Maps

Go's built-in hash table. Three operations — read, write, delete — and one surprise: iteration order is deliberately randomised. The comma-ok idiom answers "is this key present?" without ambiguity. The nil-map gotcha catches every newcomer once.


1 · The intuition

A map is a reference to a hash-table data structure managed by the Go runtime (runtime/map.go, ~2000 LOC of beautiful engineering). The header is a pointer; assigning a map to a new variable shares the underlying table — just like slices. Keys can be any comparable type (no slices, no maps, no functions as keys).

Maps vs slices, in one line. Slice = ordered, indexed by integer, values are contiguous in memory. Map = unordered, indexed by any comparable key, values are scattered through hash buckets.

2 · Try it

go main.go · the three operations
package main

import "fmt"

func main() {
    // Make a map with the literal syntax
    ages := map[string]int{
        "alice": 30,
        "bob":   25,
    }

    // READ — returns zero value if key absent (NOT an error)
    fmt.Println(ages["alice"])  // 30
    fmt.Println(ages["nobody"]) // 0  — silent zero value

    // The comma-ok idiom — distinguishes "key present" from "value is zero"
    v, ok := ages["alice"]
    fmt.Printf("alice: v=%d ok=%v
", v, ok)
    v, ok = ages["nobody"]
    fmt.Printf("nobody: v=%d ok=%v
", v, ok)

    // WRITE
    ages["carol"] = 28
    fmt.Println(ages)

    // DELETE
    delete(ages, "bob")
    fmt.Println(ages)

    // LEN
    fmt.Println("size:", len(ages))
}

3 · The nil-map panic

Declaring a map without initialising creates a nil map. You can read from it (returns zero), but writing panics. This is the most common Go beginner crash.

go main.go · the nil-map trap
package main

import "fmt"

func main() {
    var m map[string]int    // nil — no underlying table allocated
    fmt.Println(m == nil)    // true

    // Reading from a nil map: fine, returns zero
    v := m["anything"]
    fmt.Println(v)            // 0

    // Writing to a nil map: PANIC
    // m["alice"] = 1
    // Uncomment to see: panic: assignment to entry in nil map

    // Fix: use make() or a literal
    m = make(map[string]int)
    m["alice"] = 1
    fmt.Println(m)
}
The fix. Always initialise maps with make(map[K]V) or a map literal. var m map[K]V is almost always wrong unless you plan to assign to it immediately.

4 · Iteration order is RANDOM

The Go runtime randomises the starting point of every map iteration. This is a deliberate language design choice — to prevent code from accidentally depending on order. Run this twice:

go main.go · the random walk
package main

import "fmt"

func main() {
    m := map[string]int{
        "a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
    }

    for k, v := range m {
        fmt.Printf("%s=%d ", k, v)
    }
    fmt.Println()
}
// Output varies between runs:
//   b=2 d=4 a=1 e=5 c=3
//   a=1 c=3 e=5 b=2 d=4

If you need ordered iteration: collect keys, sort them, iterate the sorted slice of keys looking each value up. The standard idiom:

go main.go · ordered iteration
package main

import (
    "fmt"
    "sort"
)

func main() {
    m := map[string]int{"c": 3, "a": 1, "b": 2}

    keys := make([]string, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)

    for _, k := range keys {
        fmt.Printf("%s=%d
", k, m[k])
    }
}

5 · The "set" idiom — map[T]struct{}

Go has no built-in set type. The idiom is map[T]struct{} — the empty struct takes zero bytes per entry, so you pay only for the keys.

go main.go · sets
package main

import "fmt"

func main() {
    seen := map[string]struct{}{}  // empty struct = 0 bytes

    words := []string{"go", "rust", "go", "python", "go", "rust"}
    for _, w := range words {
        seen[w] = struct{}{}
    }

    // Membership check
    if _, ok := seen["rust"]; ok {
        fmt.Println("rust is in the set")
    }

    fmt.Println("unique count:", len(seen))
}

6 · From the wild — maps in production Go

From Kubernetes, the canonical "set of labels" pattern:

go kubernetes · pkg/labels/labels.go (paraphrased)
type Set map[string]string

// Has returns whether the provided label exists in the map.
func (ls Set) Has(label string) bool {
    _, exists := ls[label]
    return exists
}

// Get returns the value in the map for the provided label.
func (ls Set) Get(label string) string {
    return ls[label]
}

// AsSelector converts labels into a Selector — sorts keys to make it deterministic.
func (ls Set) AsSelector() Selector {
    if ls == nil || len(ls) == 0 {
        return Everything()
    }
    requirements := make([]Requirement, 0, len(ls))
    for k, v := range ls {
        requirements = append(requirements, *NewRequirement(k, "=", []string{v}))
    }
    // sort to have deterministic string representation
    sort.Sort(ByKey(requirements))
    return &internalSelector{requirements: requirements}
}
From the wild: github.com/kubernetes/kubernetes · Apache 2.0

7 · Coming from another language?

If you know…The bridge
Pythonmap[K]Vdict. The comma-ok idiom replaces Python's .get(key, default). Difference: iteration order is intentionally random in Go (Python 3.7+ guarantees insertion order).
JavaHashMap. No LinkedHashMap equivalent built in. Type-safe by default; no boxing.
JavaScriptMap (not plain object). Same operations; same general behaviour. JS Map preserves insertion order; Go doesn't.
RustHashMap<K,V>. Rust's entry() API doesn't exist in Go; you read with comma-ok, write directly.
C++std::unordered_map. std::map (sorted) has no direct Go equivalent — sort keys manually.

8 · Common mistakes

  • Writing to a nil map. var m map[K]V; m[k] = v panics. Always make or use a literal.
  • Confusing zero-value with absence. ages["nobody"] returns 0. Use comma-ok to tell apart.
  • Iterating expecting order. The runtime randomises. If you need order, sort keys first.
  • Concurrent writes. Two goroutines writing to one map panics. Use sync.Map or your own sync.RWMutex wrapper.
  • Taking a pointer to a map element. &m[k] doesn't compile — map values aren't addressable because the runtime can move buckets during resize.
  • Slice-typed keys. Won't compile. Keys must be comparable; slices aren't. Use a string-joined key, or hash to a struct.

9 · Exercises (~10 min)

  1. Word count. Read words from os.Args[1:], count occurrences, print each word and its count sorted by key.
  2. Set difference. Given a := []int{1,2,3,4,5} and b := []int{3,4,5,6,7}, produce the elements in a but not in b using the set idiom.
  3. Reproduce the nil panic. Write var m map[string]int; m["k"] = 1. Read the panic message. Fix three different ways: literal, make(), and a helper function that returns an initialised map.
  4. Concurrent-write panic. Spawn two goroutines writing to the same map. Run with go run -race main.go. Watch the race detector fire.

10 · When it clicks

  • You write seen := map[T]struct{}{} by reflex for sets.
  • You don't ever write if _, ok := m[k]; !ok { m[k] = ... } without thinking about concurrent access.
  • You can spot a "nil map panic" bug in a code review without running.
  • You know that range m never gives you the same order twice — and you defend that as a feature.
Found this useful?