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.
π― Learning Objectives
- 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.
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
- 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
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
}π Valid Map Key Types
A map key must be comparable (supports ==). Types that work as keys:
| Valid Key Types | Invalid Key Types |
|---|---|
| All numeric types (int, float64, etc.) | slice (not comparable) |
| string | map (not comparable) |
| bool, rune, byte | func (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) |
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
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.
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
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 }π€ Interview Questions
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.
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).
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
m := map[string]int{}; v := m["missing"] return?nil map?[]string as a map key?π Cheat Sheet
m := make(map[K]V)m := map[K]V{k1:v1, k2:v2}m[key] = valuev, ok := m[key]delete(m, key)len(m)for k, v := range m { }map[K]struct{}freq[word]++ // zero value is 0sort.Strings(keys); for _, k range keyssync.RWMutex or sync.MapPANIC β always make() first!