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:
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¶
- Always initialize before writing -- use
make()or a literal - Use the comma-ok idiom when zero values are valid entries
- Pre-allocate with size hint when you know the approximate entry count:
make(map[K]V, n) - Protect concurrent access with
sync.RWMutexfor general use orsync.Mapfor specific patterns - Use
map[T]struct{}for large sets to save memory - Don't take the address of map values --
&m[key]is a compile error because map values may relocate - Sort keys explicitly if you need deterministic iteration order
Common Pitfalls¶
Writing to a nil map
Fix: initialize withmake(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
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(), andrange-- but panic on write - Iteration order is deliberately random -- never depend on it
- Maps are NOT concurrent-safe -- use
sync.RWMutexorsync.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