Maps
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Go 1.27 (go1.27.2), checked on 2026-10-18. Every runnable program below was vetted with go vet,
compiled and run with that toolchain, and the output (including compiler errors and panics) is the real output. A map is Go’s built-in hash table: an unordered
collection of key-value pairs with unique keys and, on average, constant-time lookup, insertion and deletion. Together with
Arrays and Slices, it is one of the two composite types you will use
in nearly every program, and its behavior has a few rules that differ from maps and dictionaries in other languages:
reading a missing key is not an error, the iteration order is deliberately unspecified, a nil map can be read but not written,
and a map must never be used by several goroutines without synchronization. The examples use the library catalogue domain
(shelf) of this reference.
Map types, literals and make
A map type is written map[K]V, where K is the key type and V the element type. The key type must be comparable: it
must support == and !=, so slices, maps and functions cannot be keys (below). The value type is unrestricted.
Maps are reference-like: the map value is a small handle (a pointer to the runtime’s hash table), so assigning a map, passing it
to a function or storing it in a struct shares the same table. A function that receives a map can add and delete entries and the
caller sees them, unlike a slice, where appends may or may not be visible. That makes a map parameter a common source
of surprise; see Pointers and Value Semantics.
The zero value of a map is nil. You create a usable map with a composite literal or with make:
package main
import "fmt"
func main() {
stock := map[string]int{ // composite literal
"Dune": 3,
"Emma": 1,
"Ulysses": 0,
}
fmt.Println(len(stock), stock["Dune"], stock["Missing"]) // a missing key yields the zero value
n, ok := stock["Ulysses"] // comma-ok tells "stored zero" apart from "absent"
fmt.Println(n, ok)
_, ok = stock["Missing"]
fmt.Println(ok)
stock["Emma"]++ // read-modify-write works on a map element
stock["Persuasion"] = 2
delete(stock, "Ulysses")
delete(stock, "Never added") // deleting an absent key is a no-op
fmt.Println(stock) // fmt prints maps sorted by key
hinted := make(map[string]int, 1000) // size hint: preallocates, len is still 0
fmt.Println(len(hinted))
var nilMap map[string]int // zero value is nil
fmt.Println(nilMap == nil, len(nilMap), nilMap["x"])
delete(nilMap, "x") // allowed
for range nilMap { // allowed, zero iterations
}
clear(stock) // Go 1.21: delete every entry
fmt.Println(len(stock))
defer func() { fmt.Println("recovered:", recover()) }()
nilMap["x"] = 1 // writing to a nil map panics
}
3 3 0
0 true
false
map[Dune:3 Emma:2 Persuasion:2]
0
true 0 0
0
recovered: assignment to entry in nil map
Source: The Go Blog: Go maps in action
The rules shown above:
-
Lookup of a missing key yields the zero value of the element type, not an error:
stock["Missing"]is0. Use the comma-ok formv, ok := m[k]to distinguish an absent key from a stored zero value. The same idiom works for type assertions and channel receives. When you only need presence, write_, ok := m[k]. -
Assignment
m[k] = vinserts or overwrites. A compound assignment such asstock["Emma"]++reads the zero value of a missing key first, which is what makes counters and accumulators short. -
delete(m, k)removes an entry, and deleting a missing key (or from anilmap) does nothing.clear(m)(Go 1.21) removes all entries and keeps the allocated table. -
len(m)is the number of entries. A map has nocap. -
make(map[K]V, n)takes an optional size hint: the runtime preallocates room for aboutnentries. The map still has length zero, and the hint is not a limit. It avoids repeated growth when you know the final size. -
A
nilmap behaves like an empty map for reads,len,deleteandrange, but writing to it panics. -
Printing a map with
fmtshows the keys in sorted order, which is a convenience offmt(and ofencoding/json) and not a property of the map.
The size hint is a real optimization. The following program uses testing.AllocsPerRun to count the allocations
needed to insert a thousand integers into a map that grows by itself and into one that was preallocated:
package main
import (
"fmt"
"testing"
)
func main() {
const n = 1000
grow := testing.AllocsPerRun(20, func() {
m := map[int]int{}
for i := range n {
m[i] = i
}
})
hint := testing.AllocsPerRun(20, func() {
m := make(map[int]int, n)
for i := range n {
m[i] = i
}
})
fmt.Println("allocations per run, no hint:", grow, "with hint:", hint)
}
allocations per run, no hint: 20 with hint: 5
Source: testing: func AllocsPerRun
The exact counts depend on the runtime version and the platform (these are from go1.27.2 on arm64), so treat the ratio as the lesson, not the numbers; measure real code with the benchmarks described on Benchmarks, Fuzzing and synctest.
Compile errors for maps
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "a": 2}
var f map[func()]int
fmt.Println(m, f)
}
./main.go:6:30: duplicate key "a" in map literal
./main.go:7:16: invalid map key type func()
The first error is a constant key written twice in a literal; the second rejects a key type that is not comparable. A
variable that is declared but unused is also an error when ranging over a map, as in any other for loop:
package main
import "fmt"
func main() {
m := map[string]int{"a": 1}
for i, v := range m {
fmt.Println(i)
}
}
./main.go:7:9: declared and not used: v
Write for i := range m or for _, v := range m when you need only one of the two.
Key types and the comparable constraint
A key type must have == defined. That includes booleans, numbers, strings, pointers, channels, interfaces, and arrays
and structs whose elements and fields are comparable. It excludes slices, maps and functions, and any array or struct that contains
one. The compiler reports invalid map key type []int for those. An interface type is allowed as a key syntactically, but the
dynamic type of each key put into it must itself be comparable, or the program panics when the key is hashed.
package main
import "fmt"
type ISBN string
type Edition struct {
ISBN ISBN
Year int
}
func main() {
// struct keys: comparable if every field is comparable
sold := map[Edition]int{{"978-0-441-17271-9", 1965}: 12}
sold[Edition{"978-0-441-17271-9", 1965}]++
fmt.Println(sold)
// array keys
grid := map[[2]int]string{{0, 0}: "origin"}
fmt.Println(grid[[2]int{0, 0}])
// a map of slices: append to the zero value (nil) of a missing key
byAuthor := map[string][]string{}
byAuthor["Austen"] = append(byAuthor["Austen"], "Emma")
byAuthor["Austen"] = append(byAuthor["Austen"], "Persuasion")
byAuthor["Herbert"] = append(byAuthor["Herbert"], "Dune")
fmt.Println(byAuthor)
// a set: map[T]struct{} (struct{} occupies no space) or map[T]bool
seen := map[string]struct{}{}
for _, w := range []string{"a", "b", "a", "c", "b"} {
seen[w] = struct{}{}
}
_, has := seen["c"]
fmt.Println(len(seen), has)
// interface keys: the dynamic types must be hashable
anyKeys := map[any]string{1: "int", "1": "string", 1.0: "float64"}
fmt.Println(len(anyKeys), anyKeys[1], anyKeys[1.0])
defer func() { fmt.Println("recovered:", recover()) }()
anyKeys[[]int{1}] = "slice"
}
map[{978-0-441-17271-9 1965}:13]
origin
map[Austen:[Emma Persuasion] Herbert:[Dune]]
3 true
3 int float64
recovered: runtime error: hash of unhashable type []int
What this shows:
-
Struct and array keys make compound keys without string concatenation. A defined type such as
ISBNor a structEditionis usually clearer and safer than a string such as"978-...|1965". -
A map of slices works well because the zero value of a slice is
nilandappendhandlesnil:byAuthor[a] = append(byAuthor[a], title)needs no check that the key exists. -
A set is a map whose element carries no information. The empty struct
struct{}occupies zero bytes, somap[string]struct{}is the memory-lean spelling, andmap[string]boolis the more readable one when you want to writeif seen[k]. Go has no built-in set type; for a generic reusable set see Collections and Utility Packages. -
Interface keys mix key types in one map (
1,"1"and1.0are three different keys because their dynamic types areint,stringandfloat64). The panichash of unhashable type []intis raised at run time, not at compile time.
Floating-point keys
NaN is not equal to itself, so a NaN key can never be looked up, overwritten or deleted individually; every insertion
adds another entry. Avoid floating-point keys, or normalize them (round to an integer number of units, as money should be
handled anyway):
package main
import (
"fmt"
"math"
)
func main() {
m := map[float64]string{}
m[math.NaN()] = "a"
m[math.NaN()] = "b" // NaN != NaN: each insert adds a new entry
fmt.Println(len(m))
_, ok := m[math.NaN()]
fmt.Println(ok) // can never be found
n := 0
for range m {
n++
}
fmt.Println(n)
delete(m, math.NaN()) // cannot delete either
fmt.Println(len(m))
clear(m) // the only way to remove them
fmt.Println(len(m))
}
2
false
2
2
0
Only clear can remove those entries. The positive and negative zero compare equal and so are one key.
Struct values in a map are not addressable
A map element is not addressable, because the table may move it when the map grows. So you cannot assign to a field of a struct stored in a map and you cannot take the address of an element:
package main
type Loan struct{ Days int }
func main() {
loans := map[string]Loan{"l1": {14}}
loans["l1"].Days = 21
p := &loans["l1"]
_ = p
}
./main.go:7:2: cannot assign to struct field loans["l1"].Days in map
./main.go:8:8: invalid operation: cannot take address of loans["l1"] (map index expression of struct type Loan)
There are two usual fixes. Copy the value out, change it and store it back, or store pointers in the map when the elements are large or are modified in place:
package main
import "fmt"
type Loan struct{ Days int }
func main() {
loans := map[string]Loan{"l1": {14}}
l := loans["l1"] // copy out, change, store back
l.Days = 21
loans["l1"] = l
fmt.Println(loans)
ptrs := map[string]*Loan{"l1": {14}}
ptrs["l1"].Days = 28 // fine: the element is a pointer
fmt.Println(*ptrs["l1"])
}
map[l1:{21}]
{28}
A map of pointers shares each element with everyone who holds the pointer, so concurrent writes to the pointed-to
struct still need synchronization, and a *T value keeps its target alive for as long as the map holds it. See
Structs and Embedding.
Iteration
for k, v := range m visits each entry once, in an order that the spec leaves unspecified and that can differ from
one run to the next. The runtime deliberately randomizes the starting point, so code never accidentally depends on the order
that one release happened to produce. Never rely on it; if you need a stable order, collect the keys and sort them. The spec also fixes
what may happen when you change the map during the loop:
-
an entry that has not yet been reached and is removed is not produced;
-
an entry that is added may or may not be produced, and that can differ per entry and per run;
-
updating existing entries is fine.
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
m := map[string]int{"c": 3, "a": 1, "b": 2, "d": 4}
// Order is unspecified and differs from run to run; make output deterministic by sorting keys.
keys := slices.Sorted(maps.Keys(m))
for _, k := range keys {
fmt.Print(k, "=", m[k], " ")
}
fmt.Println()
// Deleting during range is safe: entries not yet reached are not produced.
for k := range m {
delete(m, k)
}
fmt.Println(len(m))
// The sum is the same regardless of order.
n := map[int]int{1: 10, 2: 20, 3: 30}
total := 0
for _, v := range n {
total += v
}
fmt.Println(total)
// Updating existing keys while ranging is fine.
for k := range n {
n[k] *= 2
}
fmt.Println(n)
}
a=1 b=2 c=3 d=4
0
60
map[1:20 2:40 3:60]
Source: The Go Blog: Go maps in action
Deleting inside the loop is therefore a safe way to remove entries based on a condition (or use maps.DeleteFunc). The sum
is the same whatever the order. Adding entries inside the loop is legal but unpredictable, so build a second map and merge it
after the loop instead. Ranging over a map can take time proportional to the allocated table rather than to len(m), so a map that was
once very large and is now almost empty is best replaced by a fresh make.
To iterate in order, collect and sort the keys. Since Go 1.23 the shortest spelling is slices.Sorted(maps.Keys(m)), which
needs no temporary variable (the older form, keys := make([]K, 0, len(m)), a loop, then sort.Strings(keys), still works):
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
type Book struct {
Title string
Author string
}
func main() {
books := []Book{
{"Emma", "Austen"}, {"Dune", "Herbert"}, {"Persuasion", "Austen"},
{"Children of Dune", "Herbert"}, {"Ulysses", "Joyce"},
}
byAuthor := make(map[string][]Book)
for _, b := range books {
byAuthor[b.Author] = append(byAuthor[b.Author], b)
}
for _, author := range slices.Sorted(maps.Keys(byAuthor)) {
titles := make([]string, 0, len(byAuthor[author]))
for _, b := range byAuthor[author] {
titles = append(titles, b.Title)
}
fmt.Printf("%-8s %s\n", author, strings.Join(titles, ", "))
}
// Count words with the zero value
counts := map[string]int{}
for _, w := range strings.Fields("a rose is a rose is a rose") {
counts[w]++
}
fmt.Println(counts)
// Set operations
a := map[string]struct{}{"x": {}, "y": {}}
b := map[string]struct{}{"y": {}, "z": {}}
union := maps.Clone(a)
maps.Copy(union, b)
inter := map[string]struct{}{}
for k := range a {
if _, ok := b[k]; ok {
inter[k] = struct{}{}
}
}
fmt.Println(slices.Sorted(maps.Keys(union)), slices.Sorted(maps.Keys(inter)))
}
Austen Emma, Persuasion
Herbert Dune, Children of Dune
Joyce Ulysses
map[a:3 is:2 rose:3]
[x y z] [y]
Source: maps: func Keys
Source: slices: func Sorted
This example also shows grouping with a map of slices, counting with the zero value, and building the union and
intersection of two sets. maps.Copy(union, b) copies all of b's pairs into union, and the intersection is a
filter loop. Remember that fmt prints maps in key order, so fmt.Println(counts) is deterministic even though the map is not.
The maps package
The maps package (Go 1.21; iterator functions in Go 1.23) removes the boilerplate loops:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
a := map[string]int{"x": 1, "y": 2}
b := maps.Clone(a) // shallow copy; Clone(nil) stays nil
b["z"] = 3
fmt.Println(a, b)
maps.Copy(a, b) // copies all pairs of b into a, overwriting
fmt.Println(a, maps.Equal(a, b))
fmt.Println(maps.EqualFunc(a, map[string]string{"x": "1", "y": "2", "z": "3"},
func(v int, s string) bool { return fmt.Sprint(v) == s }))
maps.DeleteFunc(a, func(k string, v int) bool { return v%2 == 1 })
fmt.Println(a)
// iterators (Go 1.23): Keys, Values, All, Insert, Collect
m := map[string]int{"b": 2, "a": 1, "c": 3}
fmt.Println(slices.Sorted(maps.Keys(m)))
fmt.Println(slices.Sorted(maps.Values(m)))
for _, k := range slices.Sorted(maps.Keys(m)) {
fmt.Print(k, ":", m[k], " ")
}
fmt.Println()
copyOf := maps.Collect(maps.All(m))
maps.Insert(copyOf, maps.All(map[string]int{"d": 4}))
fmt.Println(len(copyOf), maps.Equal(m, copyOf))
// build a map from a sequence of pairs
upper := maps.Collect(slices.All(strings.Fields("zero one two")))
fmt.Println(upper)
}
map[x:1 y:2] map[x:1 y:2 z:3]
map[x:1 y:2 z:3] true
true
map[y:2]
[a b c]
[1 2 3]
a:1 b:2 c:3
4 false
map[0:zero 1:one 2:two]
Source: maps package
| Function | What it does |
|---|---|
|
Shallow copy with the same entries; |
|
Copy all entries of |
|
Same keys and equal values (values must be comparable) |
|
Same keys; values compared with |
|
Delete every entry for which |
|
Iterator over key-value pairs ( |
|
Iterators over keys or values, in random order |
|
Add the pairs of an iterator to |
|
New map built from an iterator of pairs |
Notes:
-
KeysandValuesreturn iterators, not slices. Pass them toslices.Collect,slices.Sortedor arangeloop. They are in the same random order as arangeover the map. -
CloneandCopyare shallow: map or slice values (and pointers) are shared between the copies. -
slices.Allproduces pairs of(index, value), somaps.Collect(slices.All(s))builds an index-to-element map, as the last line shows. The iterator protocol is on Iterators and Range-over-Func. -
EqualandEqualFuncare the way to compare maps;==between two maps is a compile error, because a map can only be compared withnil(m == nil).reflect.DeepEqualalso compares maps but is slower.
Concurrency: maps are not safe for concurrent use
Reading a map from many goroutines is fine as long as nobody writes. But if at least one goroutine writes while others
read or write, the program has a data race, and the Go runtime may detect it and terminate the whole process with
fatal error: concurrent map writes (or concurrent map read and map write). That is not a panic: recover cannot catch it.
The detection is a best effort and non-deterministic: the following program (shown from a scratch file, not as a runnable
example, since its failure depends on scheduling) writes the same map from four goroutines:
package main
import "fmt"
func main() {
m := map[int]int{}
done := make(chan bool)
for g := 0; g < 4; g++ {
go func() {
for i := 0; i < 100000; i++ {
m[i] = i
}
done <- true
}()
}
for g := 0; g < 4; g++ {
<-done
}
fmt.Println(len(m))
}
fatal error: concurrent map writes
...
Source: The Go Blog: Go maps in action
Source: Data Race Detector
Running the same program with go run -race reports a WARNING: DATA RACE with the conflicting stacks instead, and is
the tool you should use in tests and CI; see
Memory Model and Data Races. The fix is one of:
-
guard the map with a
sync.Mutexor, for read-mostly workloads, async.RWMutex(the usual choice, wrapped in a small type that owns the map and its lock); -
confine the map to one goroutine and talk to it through channels;
-
use
sync.Mapfor the two cases it is designed for: keys written once and read many times, or goroutines that touch disjoint sets of keys. It storesanykeys and values and has methods such asLoad,Store,LoadOrStore,DeleteandRange.
package main
import (
"fmt"
"sort"
"sync"
)
// Stock is a map guarded by a mutex: the usual way to share a map.
type Stock struct {
mu sync.RWMutex
count map[string]int
}
func (s *Stock) Add(title string) {
s.mu.Lock()
defer s.mu.Unlock()
s.count[title]++
}
func (s *Stock) Count(title string) int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.count[title]
}
func main() {
st := &Stock{count: map[string]int{}}
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
for range 100 {
st.Add("Dune")
}
})
}
wg.Wait()
fmt.Println(st.Count("Dune"))
var sm sync.Map // for write-once, read-many keys or disjoint key sets
sm.Store("Emma", 1)
v, loaded := sm.LoadOrStore("Emma", 2)
fmt.Println(v, loaded)
sm.Store("Dune", 3)
var keys []string
sm.Range(func(k, _ any) bool {
keys = append(keys, k.(string))
return true
})
sort.Strings(keys)
fmt.Println(keys)
}
5000
1 true
[Dune Emma]
Source: sync: type RWMutex
Source: sync: type Map
The counter reaches exactly 5000 because every Add takes the write lock. The example uses sync.WaitGroup.Go (Go 1.25)
to start the goroutines, and range 50 (Go 1.22) to repeat. sync.Map is not a general replacement for a locked map: it has no
len, loses type safety and is slower for many workloads. See
Sync and Atomics for the full treatment.
Implementation: Swiss tables
Until Go 1.23 the built-in map was a hash table with buckets of eight entries and overflow chains. Go 1.24 replaced it with a new implementation based on the Swiss table design that Google open sourced in the Abseil C++ library. This is a runtime change with no language change: the same code runs, and Go 1.27 still uses it. The blog post describes the design:
-
The table is an open-addressed hash table: all entries live in one backing array and a collision is resolved by probing to another slot instead of chaining.
-
Slots are grouped in groups of eight, and each group has a 64-bit control word that stores a small piece of the hash for each slot (or marks it empty or deleted). A lookup compares the eight control bytes at once with a few word operations before it looks at any key, which makes both hits and misses cheap and works well with CPU caches.
-
A Go map is a collection of several independent tables, selected by high bits of the hash (extendible hashing), and one table holds at most 1024 entries. A growing map therefore grows one table at a time, which bounds the work of each insertion and keeps tail latency predictable, something a latency-sensitive server cares about.
-
The Go 1.24 release notes report a 2-3% average CPU reduction across a suite of benchmarks for the runtime changes as a whole (the new map among them). In Go 1.24 the new implementation could be disabled with the build-time setting
GOEXPERIMENT=noswissmap; the experiment flag is a diagnostic tool, not something to depend on.
Nothing in the language promises these details. The consequences you may rely on are only the spec’s: unspecified order, no addressable elements, unsafe concurrent use. See Memory Management and Garbage Collection for how maps interact with the collector (do not count on a map giving memory back after you delete entries; to release the memory of a very large map that is no longer needed, drop every reference to it or assign a fresh map).
Source: Go 1.24 release notes: Runtime
Where the books are outdated or wrong
-
Bodner (Learning Go, chapter 3) teaches maps correctly (comma-ok,
delete, the map as a set) but predates themapspackage, theclearbuiltin and iterator functions, so today you would writemaps.Clone,maps.Keyswithslices.Sortedandclear(m)where the book writes loops. It also pre-dates the Swiss table implementation. Bodner’s discussion of maps as parameters, in chapter 6, remains correct: the map handle is shared, so mutations are visible to the caller. -
Vivien (Learning Go Programming, chapter 7) describes maps as reference-like, which is accurate in effect, and correctly says the order is not guaranteed. It predates the
mapsandslicespackages,clearandsync.Map(Go 1.9, added after the book was written). It also reports all Go types as comparable in an earlier chapter; slices, maps and functions are not, which is exactly why they cannot be map keys. -
Both books pre-date generic sets and the idiom
slices.Sorted(maps.Keys(m))for ordered iteration.
Version notes
| Go release | Change |
|---|---|
1.9 |
|
1.12 |
|
1.21 |
|
1.22 |
Range over integers and per-iteration loop variables (used in the examples) |
1.23 |
|
1.24 |
New built-in map implementation based on Swiss tables |
1.25 |
|
Go 1.27 changes nothing in the language rules for maps.
Source: Go 1.12 release notes: fmt
Source: Go 1.21 release notes
Source: Go 1.23 release notes