πŸ—ΊοΈ Chapter 8 of 20

Maps

Go maps are hash tables offering O(1) average-case lookup, insertion, and deletion. Learn how they work internally, how to use them safely in concurrent programs, and all the patterns you'll need in production code.

⏱ β€”πŸ“Š Intermediate

🎯 Learning Objectives

βœ… After this chapter you will:
  • Understand Go map internals (hash table, buckets, load factor)
  • Perform all CRUD operations on maps correctly
  • Use the "comma ok" idiom to distinguish missing keys from zero values
  • Know why maps are not safe for concurrent access and how to fix it
  • Implement ordered iteration, set semantics, and frequency counting
  • Understand what types can and cannot be map keys

πŸ”¬ Map Internals

A Go map is a reference to a hash table managed by the runtime. The map header is a pointer β€” passing a map to a function passes the pointer, so the function accesses the same hash table.

πŸ“Š Go Map Internal Structure (simplified)
Map header (8 bytes β€” just a pointer):
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  *hmap       β”‚ β†’ hmap struct (on heap)
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

hmap struct:
  count:     6        ← number of key-value pairs
  B:         1        ← log2 of bucket count (2^1 = 2 buckets)
  buckets:   ptr      β†’ array of 2^B buckets

Each bucket holds up to 8 key-value pairs:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ tophash[8]  β”‚ keys[8] β”‚ values[8]  β”‚ overflow *bucket
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Lookup: hash(key) β†’ select bucket β†’ linear scan tophash β†’ compare keys
Growth: when load factor > 6.5, double bucket count and rehash
πŸ’‘ Map Complexity
  • Lookup/Insert/Delete: O(1) average, O(n) worst case (hash collision)
  • Iteration: O(n)
  • Iteration order: Intentionally randomised by Go's runtime to prevent code from accidentally depending on insertion order

πŸ—ƒοΈ CRUD Operations

go
package main

import "fmt"

func main() {
    // Create β€” always use make or literal; never assign to nil map!
    m := make(map[string]int)                   // empty map
    m2 := map[string]int{"alice": 90, "bob": 85} // map literal

    // Insert / Update
    m["alice"] = 95   // insert
    m["alice"] = 98   // update (overwrites)
    m["bob"] = 82
    m["charlie"] = 91

    // Read
    score := m["alice"]   // 98
    fmt.Println("alice:", score)

    // ❗ Reading a missing key returns the zero value β€” NOT an error!
    missing := m["dave"]  // 0 β€” dave doesn't exist
    fmt.Println("dave:", missing)  // 0 β€” misleading!

    // βœ… Use "comma ok" to distinguish missing from zero
    score, ok := m["dave"]
    if !ok {
        fmt.Println("dave not found")
    }
    score, ok = m["alice"]
    fmt.Println("alice:", score, "found:", ok)  // alice: 98 found: true

    // Delete
    delete(m, "charlie")  // removes charlie (no error if key doesn't exist)
    _, ok = m["charlie"]
    fmt.Println("charlie after delete:", ok)  // false

    // Length
    fmt.Println("count:", len(m))  // 2

    // Iterate (order is RANDOM each time)
    fmt.Println("all entries:")
    for key, val := range m {
        fmt.Printf("  %s: %d\n", key, val)
    }

    // Iterate keys only
    for key := range m {
        fmt.Println("  key:", key)
    }

    _ = m2
}
βœ… Output
alice: 98 dave: 0 dave not found alice: 98 found: true charlie after delete: false count: 2 all entries: alice: 98 bob: 82

πŸ”‘ Valid Map Key Types

A map key must be comparable (supports ==). Types that work as keys:

Valid Key TypesInvalid Key Types
All numeric types (int, float64, etc.)slice (not comparable)
stringmap (not comparable)
bool, rune, bytefunc (not comparable)
pointer (compares addresses)struct containing slice/map/func
array (elements must be comparable)interface holding non-comparable value (panics at runtime)
struct (all fields must be comparable)
interface (but panics if dynamic type is not comparable)
go
package main

import "fmt"

type Point struct{ X, Y int }

func main() {
    // struct as key
    m := map[Point]string{}
    m[Point{1, 2}] = "A"
    m[Point{3, 4}] = "B"
    fmt.Println(m[Point{1, 2}])  // A

    // array as key (useful for grids, visited sets)
    visited := map[[2]int]bool{}
    visited[[2]int{0, 0}] = true
    visited[[2]int{1, 1}] = true
    fmt.Println(visited[[2]int{0, 0}])  // true

    // interface{} key works, but dynamic type must be comparable
    im := map[interface{}]int{}
    im["hello"] = 1
    im[42] = 2
    // im[[]int{1,2}] = 3  // ← PANIC at runtime! slice is not comparable
}

⚑ Concurrent Access β€” Maps are NOT Thread-Safe

🚨 Data Race: Maps in Concurrent Code

Concurrent reads AND a write to the same map causes a data race. In Go 1.6+, the runtime detects map concurrent access and panics immediately: "concurrent map read and map write". Never access a map from multiple goroutines without synchronisation.

go
package main

import (
    "fmt"
    "sync"
)

// ❌ UNSAFE: concurrent reads and write to the same map
// func unsafeCounter() {
//     m := make(map[string]int)
//     go func() { m["a"]++ }()  // race!
//     go func() { fmt.Println(m["a"]) }()  // race!
// }

// βœ… Option 1: sync.RWMutex β€” manual locking
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func NewSafeMap() *SafeMap {
    return &SafeMap{m: make(map[string]int)}
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()
    defer sm.mu.RUnlock()
    v, ok := sm.m[key]
    return v, ok
}

func (sm *SafeMap) Set(key string, val int) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    sm.m[key] = val
}

func (sm *SafeMap) Delete(key string) {
    sm.mu.Lock()
    defer sm.mu.Unlock()
    delete(sm.m, key)
}

// βœ… Option 2: sync.Map β€” built-in concurrent map
// Best when: keys are written once and read many times
func syncMapDemo() {
    var sm sync.Map

    // Store
    sm.Store("alice", 95)
    sm.Store("bob", 82)

    // Load
    if v, ok := sm.Load("alice"); ok {
        fmt.Println("alice:", v.(int))  // type assert needed
    }

    // LoadOrStore: atomic "get or set"
    actual, loaded := sm.LoadOrStore("charlie", 88)
    fmt.Println("charlie:", actual, "already existed:", loaded)

    // Iterate
    sm.Range(func(key, value interface{}) bool {
        fmt.Printf("  %v: %v\n", key, value)
        return true  // return false to stop iteration
    })

    // Delete
    sm.Delete("bob")
}

func main() {
    sm := NewSafeMap()

    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            sm.Set(fmt.Sprintf("key%d", n), n)
        }(i)
    }
    wg.Wait()

    fmt.Println("safe map has entries:", len(sm.m))

    syncMapDemo()
}

🎨 Common Map Patterns

go
package main

import (
    "fmt"
    "sort"
)

func main() {
    // ── Frequency counter ──
    words := []string{"go", "is", "fast", "go", "is", "go"}
    freq := make(map[string]int)
    for _, w := range words {
        freq[w]++  // increment (zero value of int is 0, so first access = 0+1 = 1)
    }
    fmt.Println("freq:", freq)  // map[fast:1 go:3 is:2]

    // ── Set semantics (map[T]struct{}) ──
    // struct{} takes 0 bytes β€” the most memory-efficient set
    set := make(map[string]struct{})
    for _, w := range words {
        set[w] = struct{}{}
    }
    _, exists := set["go"]
    fmt.Println("go in set:", exists)  // true

    // ── Grouping / multi-value map ──
    type Student struct{ Name, Class string }
    students := []Student{
        {"Alice", "A"}, {"Bob", "B"}, {"Charlie", "A"}, {"Dave", "B"},
    }
    byClass := make(map[string][]Student)
    for _, s := range students {
        byClass[s.Class] = append(byClass[s.Class], s)
    }
    fmt.Println("Class A:", byClass["A"])

    // ── Ordered iteration (sort keys) ──
    m := map[string]int{"banana": 2, "apple": 5, "cherry": 1}
    keys := make([]string, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    fmt.Println("sorted:")
    for _, k := range keys {
        fmt.Printf("  %s: %d\n", k, m[k])
    }

    // ── Default values / accumulation ──
    // Count character frequencies in a string
    charFreq := make(map[rune]int)
    for _, r := range "hello world" {
        charFreq[r]++
    }
    fmt.Println("'l' count:", charFreq['l'])   // 3
    fmt.Println("'o' count:", charFreq['o'])   // 2

    // ── Invert a map ──
    original := map[string]int{"a": 1, "b": 2, "c": 3}
    inverted := make(map[int]string, len(original))
    for k, v := range original {
        inverted[v] = k
    }
    fmt.Println("inverted:", inverted)  // map[1:a 2:b 3:c]

    // ── Check and set (if not exists) ──
    cache := make(map[string]int)
    key := "result"
    if _, ok := cache[key]; !ok {
        cache[key] = expensiveCompute()
    }
    fmt.Println("cached:", cache[key])
}

func expensiveCompute() int { return 42 }
βœ… Output
freq: map[fast:1 go:3 is:2] go in set: true Class A: [{Alice A} {Charlie A}] sorted: apple: 5 banana: 2 cherry: 1 'l' count: 3 'o' count: 2 inverted: map[1:a 2:b 3:c] cached: 42

🎀 Interview Questions

1
Why is it dangerous to read from a missing map key without the "comma ok" idiom?
β–Ά
Detailed Answer

Reading a missing key returns the zero value of the value type silently β€” no error, no panic. For a map[string]int, a missing key returns 0. If you're counting occurrences, 0 is also a valid "was set to zero" state, making it impossible to distinguish "not found" from "exists with value 0".

The "comma ok" idiom v, ok := m[key] returns a boolean ok that is true only if the key exists. This is the only correct way to check for map key existence.

This is especially dangerous when: (1) the value type has a meaningful zero value (int counter, bool flag); (2) you're using a map as a cache; (3) you're building a permission or configuration system.

2
Why are maps not safe for concurrent access? What are your options?
β–Ά
Detailed Answer

Go maps are not thread-safe because they use internal structures (buckets, overflow pointers) that can be corrupted by concurrent writes without proper synchronisation. Go 1.6 added a runtime check that panics on concurrent map access.

Options: (1) sync.Mutex: wrap the map in a struct with a mutex. Use Lock() for writes, RLock() for reads. Simple, explicit, good for complex operations; (2) sync.RWMutex: allows multiple concurrent readers but exclusive writers β€” better for read-heavy workloads; (3) sync.Map: built-in concurrent map optimised for specific patterns (write-once-read-many, or disjoint key sets); (4) channel-based serialisation: have a single goroutine own the map and communicate via channels.

When to use what: For most use cases, sync.RWMutex + regular map is the most flexible. Use sync.Map only when its specific optimisations apply (its API is more awkward β€” no generics, requires type assertions).

3
What is the time complexity of map operations? Is iteration order guaranteed?
β–Ά
Detailed Answer

Lookup, insertion, and deletion are O(1) average case. In the worst case (all keys hash to the same bucket), it degrades to O(n), but the Go runtime uses a good hash function with random seed to make this practically impossible.

Iteration is O(n). Crucially, iteration order is intentionally randomised. The Go team deliberately randomises map iteration to prevent developers from writing code that silently depends on a particular order (which would be fragile). If you need ordered iteration, extract the keys, sort them, then iterate in sorted order.

πŸ§ͺ Quiz

🧩
Maps
5 questions
1. What does m := map[string]int{}; v := m["missing"] return?
2. What happens when you write to a nil map?
3. What is the guaranteed iteration order of a Go map?
4. Can you use a []string as a map key?
5. What is the most memory-efficient way to implement a set in Go?
β€”

πŸ“Œ Cheat Sheet

πŸ“Œ Maps β€” Quick Reference
Make
m := make(map[K]V)
Literal
m := map[K]V{k1:v1, k2:v2}
Set
m[key] = value
Get + check
v, ok := m[key]
Delete
delete(m, key)
Length
len(m)
Iterate
for k, v := range m { }
Set type
map[K]struct{}
Counter
freq[word]++ // zero value is 0
Ordered iter
sort.Strings(keys); for _, k range keys
Thread-safe
sync.RWMutex or sync.Map
Nil map write
PANIC β€” always make() first!