Day 6 — Maps
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.
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 comparablesTheory 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 // OKLaw: 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 zeroComma-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-opUpdate 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 backOr store pointers:
m := map[string]*Counter{"a": {N: 1}}
m["a"].N++ // OKTheory 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"]) // 2func 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"] // falseExample 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/day06Spec:
- Read stdin or a file path argument.
- Split on
unicode/strings.Fields(whitespace).
- Normalize with
strings.ToLower.
- Print
word countlines sorted by count descending, then word ascending.
- Exit non-zero on I/O errors.
echo "Go go Gopher gopher GO" | go run .
# go 3
# gopher 2Lab 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 bUse 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
deleteandlenused
- 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.