Skip to content

Maps Beginner

Introduction

Maps are Go's built-in hash table implementation, providing average O(1) lookups, inserts, and deletes. They are reference types -- passing a map to a function gives it access to the same underlying data. Key interview topics include the comma-ok idiom, nil map panics, random iteration order, and the fact that maps are not safe for concurrent use.


Declaration and Initialization

// Using make (preferred when you'll populate dynamically)
m := make(map[string]int)        // empty, non-nil map
m2 := make(map[string]int, 100)  // hint: pre-allocate ~100 entries

// Map literal (preferred when you know initial data)
m3 := map[string]int{
    "alice": 95,
    "bob":   87,
    "carol": 92,  // trailing comma required
}

// Nil map (declaration without initialization)
var m4 map[string]int  // nil -- reads return zero value, writes PANIC

Accessing Values: The Comma-Ok Idiom

Reading a missing key returns the zero value of the value type, which makes it impossible to distinguish "key exists with zero value" from "key doesn't exist" without the second return value.

m := map[string]int{"alice": 95, "bob": 0}

// Simple access -- can't tell if bob scored 0 or doesn't exist
score := m["bob"]   // 0
score2 := m["dave"] // 0 -- same result, but dave doesn't exist

// Comma-ok idiom -- the idiomatic way
score, ok := m["bob"]
if ok {
    fmt.Println("bob scored", score) // bob scored 0
}

// Common pattern: check and act
if v, ok := m[key]; ok {
    // key exists, use v
} else {
    // key doesn't exist
}

Interview Tip

The comma-ok idiom is one of Go's most recognizable patterns. Always use v, ok := m[key] when the zero value is a valid entry (counters, booleans, empty strings).


Writing, Deleting, and Iterating

m := map[string]int{}

// Write
m["alice"] = 95

// Delete (no-op if key doesn't exist, safe on nil map)
delete(m, "alice")
delete(m, "nonexistent") // no error, no panic

// Iterate -- order is RANDOMIZED by the runtime
for key, value := range m {
    fmt.Printf("%s: %d\n", key, value)
}

// Keys only
for key := range m {
    fmt.Println(key)
}

// Clear all entries (Go 1.21+)
clear(m) // m is now empty but non-nil

Iteration Order Is Random

Go deliberately randomizes map iteration order to prevent code from depending on it. If you need sorted output, collect keys into a slice and sort first:

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


Nil Map vs Empty Map

var nilMap map[string]int     // nil
emptyMap := map[string]int{}  // non-nil, empty
madeMap := make(map[string]int) // non-nil, empty

// Reading: all three behave the same
_ = nilMap["key"]            // 0, no panic
_, _ = nilMap["key"]         // 0, false -- no panic
len(nilMap)                  // 0
delete(nilMap, "key")        // no-op, no panic

// Writing: nil map PANICS
// nilMap["key"] = 1         // panic: assignment to entry in nil map
emptyMap["key"] = 1          // works fine

Nil Map Write Panic

Reading from a nil map is safe (returns zero value). Writing to a nil map causes a runtime panic. Always initialize maps before writing: m := make(map[K]V) or m := map[K]V{}.


Maps Are Reference Types

func populate(m map[string]int) {
    m["added"] = 42 // modifies the caller's map
}

original := map[string]int{"a": 1}
populate(original)
fmt.Println(original["added"]) // 42

The map variable is a pointer to the underlying hash table structure. Passing it to a function or assigning it to another variable does not copy the data.


Map as Set

Go has no built-in set type. Use map[T]bool or map[T]struct{}.

// Using bool (readable, slightly more memory)
seen := map[string]bool{}
seen["alice"] = true
if seen["alice"] {
    fmt.Println("already seen")
}

// Using struct{} (zero memory per entry, more idiomatic for large sets)
type void struct{}
set := map[string]void{}
set["alice"] = void{}
if _, ok := set["alice"]; ok {
    fmt.Println("already seen")
}

Why struct{}?

struct{} has zero size. map[string]struct{} uses less memory than map[string]bool because boolean values occupy at least 1 byte each. For small maps the difference is negligible.


Map of Maps and Struct Keys

// Map of maps -- inner map must be initialized before use
graph := make(map[string]map[string]int)
if graph["a"] == nil {
    graph["a"] = make(map[string]int)
}
graph["a"]["b"] = 5

// Struct as key (all fields must be comparable)
type Point struct {
    X, Y int
}
visited := map[Point]bool{
    {0, 0}: true,
    {1, 2}: true,
}

Map Key Requirements

Map keys must be comparable (== and !=). This includes: booleans, numbers, strings, pointers, channels, arrays of comparable types, and structs where all fields are comparable. Slices, maps, and functions cannot be map keys.


Concurrency: Maps Are NOT Thread-Safe

// UNSAFE -- concurrent map read/write causes fatal panic (not a data race, a CRASH)
m := make(map[string]int)
go func() { m["a"] = 1 }()
go func() { _ = m["a"] }()
// fatal error: concurrent map read and map write

// OPTION 1: sync.Mutex
var mu sync.Mutex
mu.Lock()
m["a"] = 1
mu.Unlock()

// OPTION 2: sync.RWMutex (better for read-heavy workloads)
var rw sync.RWMutex
rw.RLock()
_ = m["a"]  // multiple readers allowed
rw.RUnlock()

// OPTION 3: sync.Map (optimized for specific patterns)
var sm sync.Map
sm.Store("a", 1)
val, ok := sm.Load("a")
sm.Range(func(key, value any) bool {
    fmt.Println(key, value)
    return true // continue iteration
})

Concurrent Map Access Is Fatal

Unlike data races on other types (which cause undefined behavior silently), concurrent map access in Go triggers a runtime panic with the message concurrent map read and map write. The runtime has built-in detection.

When to Use sync.Map

sync.Map is optimized for two patterns: (1) write-once, read-many (like a cache that's populated at startup) and (2) disjoint key sets per goroutine. For all other patterns, a regular map with sync.RWMutex performs better.


Quick Reference

Operation Syntax Nil Map Behavior
Create make(map[K]V) or map[K]V{} --
Read v := m[k] Returns zero value
Comma-ok v, ok := m[k] ok is false
Write m[k] = v PANIC
Delete delete(m, k) No-op
Length len(m) 0
Iterate for k, v := range m No iterations
Clear clear(m) No-op

Best Practices

  1. Always initialize before writing -- use make() or a literal
  2. Use the comma-ok idiom when zero values are valid entries
  3. Pre-allocate with size hint when you know the approximate entry count: make(map[K]V, n)
  4. Protect concurrent access with sync.RWMutex for general use or sync.Map for specific patterns
  5. Use map[T]struct{} for large sets to save memory
  6. Don't take the address of map values -- &m[key] is a compile error because map values may relocate
  7. Sort keys explicitly if you need deterministic iteration order

Common Pitfalls

Writing to a nil map

var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
Fix: initialize with make(map[string]int) or a literal.

Assuming iteration order

Map iteration order is randomized and changes between runs. Never assume keys come out in insertion order, alphabetical order, or any order.

Taking address of map values

m := map[string]User{"alice": {Name: "Alice"}}
// p := &m["alice"]       // compile error!
// m["alice"].Name = "Bob" // compile error! (can't assign to struct field in map)

// Fix: copy out, modify, write back
u := m["alice"]
u.Name = "Bob"
m["alice"] = u

Growing maps don't shrink

Adding millions of entries then deleting them does not free the underlying memory. If you need to reclaim memory, create a new map and copy the remaining entries.


Performance Considerations

Operation Average Worst Case Notes
Lookup O(1) O(n) Worst case with many hash collisions
Insert O(1) O(n) Amortized; may trigger rehash
Delete O(1) O(n) Doesn't shrink memory
Iteration O(n) O(n) n = bucket count, not entry count

Pre-allocation Matters

make(map[K]V, n) avoids repeated rehashing during population. For building maps with thousands of entries, pre-allocation can be 30-50% faster.


Interview Tips

Interview Tip

When asked "What happens if you read from a nil map?", the answer is: it returns the zero value of the value type with no panic. Only writing to a nil map panics.

Interview Tip

If asked about concurrent map safety, explain all three options: sync.Mutex (simple), sync.RWMutex (read-heavy optimization), sync.Map (specific patterns only). Most production code uses RWMutex.

Interview Tip

Know why you can't do m["key"].Field = value for struct values in maps: map values are not addressable because the runtime may relocate them during growth. You must copy out, modify, and write back -- or use a map of pointers (map[K]*V).


Key Takeaways

  • Maps are reference types backed by a hash table with O(1) average operations
  • Use the comma-ok idiom (v, ok := m[k]) to distinguish missing keys from zero values
  • Nil maps are safe to read, len(), delete(), and range -- but panic on write
  • Iteration order is deliberately random -- never depend on it
  • Maps are NOT concurrent-safe -- use sync.RWMutex or sync.Map
  • Map values are not addressable -- you can't modify struct fields in place
  • Pre-allocate with a size hint for better performance on large maps