Day 6 — Maps

Updated

July 30, 2026

Day 6 — Maps

Stage I · ~3h (theory-heavy)
Goal: Use maps idiomatically for counts, indexes, and simple caches—mastering nil vs made maps, the comma-ok idiom, deletion, and the rule that iteration order is intentionally randomized.

Note

Maps are not safe for concurrent read/write without synchronization. Today you stay single-goroutine. Concurrent map use returns with the race detector in Stage III.

Why this day exists

Maps appear everywhere:

  • Frequency counts and histograms
  • Deduplication sets (map[T]struct{})
  • Indexes by ID
  • Memoization / caches
  • JSON objects decoding to map[string]any

Misunderstanding zero values, missing keys, and iteration order produces flaky tests and subtle production bugs. Day 6 locks the model.


Theory 1 — Type and creation

var m map[string]int          // nil map
m2 := map[string]int{}        // empty non-nil
m3 := make(map[string]int)    // empty non-nil
m4 := make(map[string]int, 100) // hint capacity
m5 := map[string]int{"a": 1, "b": 2}

Key and value types

  • Keys must be comparable (can use ==): numbers, strings, pointers, channels, interfaces, structs/arrays of comparable types
  • Slices, maps, and functions are not comparable → cannot be keys
  • Values may be any type, including slices and maps
type Coord struct{ X, Y int }
scores := map[Coord]int{{1, 2}: 10} // OK: struct of comparables

Theory 2 — Nil map vs empty map

Operation Nil map Empty made map
Read m[k] zero value zero value
len(m) 0 0
Write m[k]=v panic OK
delete(m, k) no-op OK
range no iterations no iterations
var m map[string]int
fmt.Println(m["x"]) // 0, no panic
// m["x"] = 1       // panic: assignment to entry in nil map
m = make(map[string]int)
m["x"] = 1          // OK

Law: always make (or literal) before first write.


Theory 3 — Lookup, comma-ok, and zero values

v := m["missing"] // 0 for int — cannot tell missing from stored zero

Comma-ok idiom

v, ok := m["missing"]
if !ok {
    // key absent
}
ok Meaning
true Key present (value may still be zero)
false Key absent; v is zero value

Presence-only sets

seen := make(map[string]struct{})
seen["a"] = struct{}{}
if _, ok := seen["a"]; ok {
    // present
}

struct{} uses no memory per value beyond the map’s own overhead of tracking the key.


Theory 4 — Mutation: assign, delete, update

m["a"] = 1
m["a"]++        // read-modify-write
delete(m, "a")  // safe if absent
delete(m, "z")  // no-op

Update with comma-ok

func increment(m map[string]int, k string) {
    m[k]++ // works even if absent: zero then +1
}

For non-increment cases:

if v, ok := m[k]; ok {
    m[k] = v + 10
} else {
    m[k] = 10
}

Maps hold values, not variables

type Counter struct{ N int }
m := map[string]Counter{"a": {N: 1}}
// m["a"].N++ // compile error: cannot assign to struct field in map
c := m["a"]
c.N++
m["a"] = c // write back

Or store pointers:

m := map[string]*Counter{"a": {N: 1}}
m["a"].N++ // OK

Theory 5 — Iteration order is random

m := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
    fmt.Println(k, v)
}

Go randomizes map iteration order so programs do not accidentally depend on it. Tests that compare full map dump strings without sorting will flake.

Stable output pattern

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\n", k, m[k])
}

Theory 6 — Reference semantics

Map values are descriptors (like slices headers). Assignment and function parameters share the same underlying map:

a := map[string]int{"x": 1}
b := a
b["x"] = 2
fmt.Println(a["x"]) // 2
func clearAll(m map[string]int) {
    for k := range m {
        delete(m, k)
    }
}

To deep-copy:

func clone(m map[string]int) map[string]int {
    out := make(map[string]int, len(m))
    for k, v := range m {
        out[k] = v
    }
    return out
}

Theory 7 — Design patterns

Word count

func wordCount(text string) map[string]int {
    freq := make(map[string]int)
    for _, w := range strings.Fields(strings.ToLower(text)) {
        freq[w]++
    }
    return freq
}

Index by ID

func indexByID(users []User) map[string]User {
    m := make(map[string]User, len(users))
    for _, u := range users {
        m[u.ID] = u
    }
    return m
}

Simple TTL-less cache

type Cache struct {
    m map[string]string
}

func NewCache() *Cache {
    return &Cache{m: make(map[string]string)}
}

func (c *Cache) Get(k string) (string, bool) {
    v, ok := c.m[k]
    return v, ok
}

func (c *Cache) Set(k, v string) {
    c.m[k] = v
}

Not concurrent-safe. Good enough for single-threaded CLIs.

Grouping

func groupBy[T any, K comparable](items []T, key func(T) K) map[K][]T {
    out := make(map[K][]T)
    for _, item := range items {
        k := key(item)
        out[k] = append(out[k], item)
    }
    return out
}

Generics formal depth is Stage IV; the pattern works with concrete types today.


Worked examples bank

Example A — Nil write panic (observe once)

func main() {
    var m map[string]int
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("recovered:", r)
        }
    }()
    m["x"] = 1
}

Prefer fixing with make over recovering (Day 14).

Example B — Comma-ok vs zero value

m := map[string]int{"a": 0}
fmt.Println(m["a"])        // 0
fmt.Println(m["missing"])  // 0
_, ok1 := m["a"]           // true
_, ok2 := m["missing"]     // false

Example C — Frequency report sorted

func report(freq map[string]int) {
    type kv struct {
        k string
        v int
    }
    var list []kv
    for k, v := range freq {
        list = append(list, kv{k, v})
    }
    sort.Slice(list, func(i, j int) bool {
        if list[i].v != list[j].v {
            return list[i].v > list[j].v // count desc
        }
        return list[i].k < list[j].k
    })
    for _, e := range list {
        fmt.Printf("%s %d\n", e.k, e.v)
    }
}

Example D — Dedup preserving first-seen order

func unique(ss []string) []string {
    seen := make(map[string]struct{}, len(ss))
    out := make([]string, 0, len(ss))
    for _, s := range ss {
        if _, ok := seen[s]; ok {
            continue
        }
        seen[s] = struct{}{}
        out = append(out, s)
    }
    return out
}

Example E — Nested maps

// map[region]map[service]count
func inc(m map[string]map[string]int, region, service string) {
    inner, ok := m[region]
    if !ok {
        inner = make(map[string]int)
        m[region] = inner
    }
    inner[service]++
}

Always ensure the inner map is made before write.


Labs

Suggested workspace: ~/lab/90daysofx/go/day06

Lab 1 — Word count CLI

mkdir -p ~/lab/90daysofx/go/day06
cd ~/lab/90daysofx/go/day06
go mod init example.com/day06

Spec:

  1. Read stdin or a file path argument.
  2. Split on unicode/strings.Fields (whitespace).
  3. Normalize with strings.ToLower.
  4. Print word count lines sorted by count descending, then word ascending.
  5. Exit non-zero on I/O errors.
echo "Go go Gopher gopher GO" | go run .
# go 3
# gopher 2

Lab 2 — Set operations

Implement:

func union(a, b []string) []string
func intersect(a, b []string) []string
func difference(a, b []string) []string // in a not in b

Use maps. Document whether outputs are sorted (they should be for stable demos).

Lab 3 — Mini cache

In-memory string cache with Get/Set/Delete/Len. Drive it from a tiny REPL or flags:

set foo bar
get foo  → bar
get baz  → missing (exit or print status)

Common gotchas

Gotcha Fix
Write to nil map make first
Using value for presence when zero is valid Comma-ok
Flaky tests on map range order Sort keys
Concurrent map access Mutex / sync.Map later—not today
Cannot increment struct field in map Copy out, modify, write back—or store *T
Using slice as map key Use string key or comparable struct
Expecting maps to deep-copy on assign They share; clone explicitly

Checkpoint

  • Explain nil vs empty map
  • Comma-ok used correctly
  • delete and len used
  • Sorted output for deterministic CLI
  • Word count lab works
  • Know maps are reference-like and not concurrent-safe

Commit

git add .
git commit -m "day06: maps wordcount + set ops"

Tomorrow

Day 7 — Structs, embedding & methods: domain modeling with composite types, value vs pointer receivers, and promotion via embedding—without mistaking embedding for inheritance.