Maps in Go: Lookups, Iteration Order and the nil Map Trap
Creating and reading maps, the comma-ok idiom, random iteration order, how maps behave when passed to functions, and concurrency safety.
On this page
A map stores key-value pairs with fast lookup, insert and delete (O(1) on average). It is Go's built-in hash table, and you will use it for caches, counters, indexes and grouping data.
Creating maps
The type is map[KeyType]ValueType.
colors := map[string]string{
"red": "#ff0000",
"green": "#00ff00",
"blue": "#0000ff",
}
ages := make(map[string]int) // empty, ready to use
ages["sabbir"] = 28
hint := make(map[int]string, 1000) // size hint: fewer reallocations when you know roughly how many entriesKeys must be comparable with ==: strings, numbers, booleans, pointers, channels, arrays and structs made of comparable fields. Slices, maps and functions cannot be keys.
Reading: the comma-ok idiom
Reading a missing key does not fail. It returns the zero value of the value type. To tell "missing" apart from "present but zero", use the two-value form:
hex, ok := colors["purple"]
if !ok {
fmt.Println("purple is not defined")
}This makes counting very clean, because a missing key starts at 0:
counts := map[string]int{}
for _, word := range []string{"go", "is", "go"} {
counts[word]++
}
fmt.Println(counts) // map[go:2 is:1]Updating, deleting, length
colors["red"] = "#f00" // insert or update
delete(colors, "green") // remove (deleting a missing key is fine)
fmt.Println(len(colors)) // number of entriesIteration order is random
for color, hex := range colors {
fmt.Printf("%s -> %s\n", color, hex)
}Go deliberately randomises map iteration order, so code cannot accidentally depend on it. When you need a stable order (for output, tests or APIs), sort the keys first:
import (
"maps"
"slices"
)
for _, k := range slices.Sorted(maps.Keys(colors)) { // Go 1.23+
fmt.Println(k, colors[k])
}fmt.Println(colors) itself prints keys in sorted order, which is convenient for debugging.
Maps are passed "by reference"... sort of
Everything in Go is passed by value. A map value, however, is a small pointer to the runtime's hash table, so copying the map variable copies the pointer, not the data. A function that receives a map can modify the caller's entries:
func paint(c map[string]string) {
c["red"] = "#ff0000-changed" // visible to the caller
}
func reassign(c map[string]string) {
c = map[string]string{"new": "map"} // only changes the local variable
}
colors := map[string]string{"red": "#ff0000"}
paint(colors)
reassign(colors)
fmt.Println(colors) // map[red:#ff0000-changed]Adding or changing entries is visible; pointing the parameter at a brand-new map is not.
The nil map trap
A declared but uninitialised map is nil. Reading from it is allowed, but writing panics:
var m map[string]int
fmt.Println(m["x"], len(m)) // 0 0, reads are fine
m["x"] = 1 // panic: assignment to entry in nil mapAlways create maps with a literal or make before writing. In structs, initialise map fields in a constructor function.
Maps vs structs
Use a struct when the fields are known at compile time (a User always has Name and Email). Use a map when keys are dynamic data (counting words, caching by ID). Structs are type-checked and faster; maps are flexible. You can range over a map, but not over a struct's fields (without reflection).
Concurrency warning
fatal error: concurrent map writes. Protect the map with a sync.Mutex / sync.RWMutex, or use sync.Map for the specific case of mostly-read, write-once keys.Key takeaways
make or a literal; writing to a nil map panics.v, ok := m[k] distinguishes a missing key from a zero value.Last updated on