Maps
Go's built-in hash table. Three operations — read, write, delete — and one surprise: iteration order is deliberately randomised. The comma-ok idiom answers "is this key present?" without ambiguity. The nil-map gotcha catches every newcomer once.
1 · The intuition
A map is a reference to a hash-table data structure managed by the Go runtime
(runtime/map.go, ~2000 LOC of beautiful engineering). The header
is a pointer; assigning a map to a new variable shares the underlying table —
just like slices. Keys can be any comparable type (no slices, no maps,
no functions as keys).
2 · Try it
package main
import "fmt"
func main() {
// Make a map with the literal syntax
ages := map[string]int{
"alice": 30,
"bob": 25,
}
// READ — returns zero value if key absent (NOT an error)
fmt.Println(ages["alice"]) // 30
fmt.Println(ages["nobody"]) // 0 — silent zero value
// The comma-ok idiom — distinguishes "key present" from "value is zero"
v, ok := ages["alice"]
fmt.Printf("alice: v=%d ok=%v
", v, ok)
v, ok = ages["nobody"]
fmt.Printf("nobody: v=%d ok=%v
", v, ok)
// WRITE
ages["carol"] = 28
fmt.Println(ages)
// DELETE
delete(ages, "bob")
fmt.Println(ages)
// LEN
fmt.Println("size:", len(ages))
}3 · The nil-map panic
Declaring a map without initialising creates a nil map. You can read from it (returns zero), but writing panics. This is the most common Go beginner crash.
package main
import "fmt"
func main() {
var m map[string]int // nil — no underlying table allocated
fmt.Println(m == nil) // true
// Reading from a nil map: fine, returns zero
v := m["anything"]
fmt.Println(v) // 0
// Writing to a nil map: PANIC
// m["alice"] = 1
// Uncomment to see: panic: assignment to entry in nil map
// Fix: use make() or a literal
m = make(map[string]int)
m["alice"] = 1
fmt.Println(m)
}make(map[K]V)
or a map literal. var m map[K]V is almost always wrong unless you
plan to assign to it immediately.4 · Iteration order is RANDOM
The Go runtime randomises the starting point of every map iteration. This is a deliberate language design choice — to prevent code from accidentally depending on order. Run this twice:
package main
import "fmt"
func main() {
m := map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
}
for k, v := range m {
fmt.Printf("%s=%d ", k, v)
}
fmt.Println()
}
// Output varies between runs:
// b=2 d=4 a=1 e=5 c=3
// a=1 c=3 e=5 b=2 d=4If you need ordered iteration: collect keys, sort them, iterate the sorted slice of keys looking each value up. The standard idiom:
package main
import (
"fmt"
"sort"
)
func main() {
m := map[string]int{"c": 3, "a": 1, "b": 2}
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
", k, m[k])
}
}5 · The "set" idiom — map[T]struct{}
Go has no built-in set type. The idiom is map[T]struct{}
— the empty struct takes zero bytes per entry, so you pay only for the keys.
package main
import "fmt"
func main() {
seen := map[string]struct{}{} // empty struct = 0 bytes
words := []string{"go", "rust", "go", "python", "go", "rust"}
for _, w := range words {
seen[w] = struct{}{}
}
// Membership check
if _, ok := seen["rust"]; ok {
fmt.Println("rust is in the set")
}
fmt.Println("unique count:", len(seen))
}6 · From the wild — maps in production Go
From Kubernetes, the canonical "set of labels" pattern:
type Set map[string]string
// Has returns whether the provided label exists in the map.
func (ls Set) Has(label string) bool {
_, exists := ls[label]
return exists
}
// Get returns the value in the map for the provided label.
func (ls Set) Get(label string) string {
return ls[label]
}
// AsSelector converts labels into a Selector — sorts keys to make it deterministic.
func (ls Set) AsSelector() Selector {
if ls == nil || len(ls) == 0 {
return Everything()
}
requirements := make([]Requirement, 0, len(ls))
for k, v := range ls {
requirements = append(requirements, *NewRequirement(k, "=", []string{v}))
}
// sort to have deterministic string representation
sort.Sort(ByKey(requirements))
return &internalSelector{requirements: requirements}
}7 · Coming from another language?
| If you know… | The bridge |
|---|---|
| Python | map[K]V ≈ dict. The comma-ok idiom replaces Python's .get(key, default). Difference: iteration order is intentionally random in Go (Python 3.7+ guarantees insertion order). |
| Java | ≈ HashMap. No LinkedHashMap equivalent built in. Type-safe by default; no boxing. |
| JavaScript | ≈ Map (not plain object). Same operations; same general behaviour. JS Map preserves insertion order; Go doesn't. |
| Rust | ≈ HashMap<K,V>. Rust's entry() API doesn't exist in Go; you read with comma-ok, write directly. |
| C++ | ≈ std::unordered_map. std::map (sorted) has no direct Go equivalent — sort keys manually. |
8 · Common mistakes
- Writing to a nil map.
var m map[K]V; m[k] = vpanics. Alwaysmakeor use a literal. - Confusing zero-value with absence.
ages["nobody"]returns 0. Use comma-ok to tell apart. - Iterating expecting order. The runtime randomises. If you need order, sort keys first.
- Concurrent writes. Two goroutines writing to one map panics. Use
sync.Mapor your ownsync.RWMutexwrapper. - Taking a pointer to a map element.
&m[k]doesn't compile — map values aren't addressable because the runtime can move buckets during resize. - Slice-typed keys. Won't compile. Keys must be comparable; slices aren't. Use a string-joined key, or hash to a struct.
9 · Exercises (~10 min)
- Word count. Read words from
os.Args[1:], count occurrences, print each word and its count sorted by key. - Set difference. Given
a := []int{1,2,3,4,5}andb := []int{3,4,5,6,7}, produce the elements inabut not inbusing the set idiom. - Reproduce the nil panic. Write
var m map[string]int; m["k"] = 1. Read the panic message. Fix three different ways: literal,make(), and a helper function that returns an initialised map. - Concurrent-write panic. Spawn two goroutines writing to the same map. Run with
go run -race main.go. Watch the race detector fire.
10 · When it clicks
- You write
seen := map[T]struct{}{}by reflex for sets. - You don't ever write
if _, ok := m[k]; !ok { m[k] = ... }without thinking about concurrent access. - You can spot a "nil map panic" bug in a code review without running.
- You know that
range mnever gives you the same order twice — and you defend that as a feature.