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

The rules shown above:

  • Lookup of a missing key yields the zero value of the element type, not an error: stock["Missing"] is 0. Use the comma-ok form v, 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] = v inserts or overwrites. A compound assignment such as stock["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 a nil map) 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 no cap.

  • make(map[K]V, n) takes an optional size hint: the runtime preallocates room for about n entries. The map still has length zero, and the hint is not a limit. It avoids repeated growth when you know the final size.

  • A nil map behaves like an empty map for reads, len, delete and range, but writing to it panics.

  • Printing a map with fmt shows the keys in sorted order, which is a convenience of fmt (and of encoding/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

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 ISBN or a struct Edition is 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 nil and append handles nil: 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, so map[string]struct{} is the memory-lean spelling, and map[string]bool is the more readable one when you want to write if 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" and 1.0 are three different keys because their dynamic types are int, string and float64). The panic hash of unhashable type []int is 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]

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

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

maps.Clone(m)

Shallow copy with the same entries; Clone(nil) returns nil

maps.Copy(dst, src)

Copy all entries of src into dst, overwriting keys that already exist

maps.Equal(a, b)

Same keys and equal values (values must be comparable)

maps.EqualFunc(a, b, eq)

Same keys; values compared with eq (the types may differ)

maps.DeleteFunc(m, del)

Delete every entry for which del(k, v) is true

maps.All(m)

Iterator over key-value pairs (iter.Seq2[K, V]), in random order

maps.Keys(m), maps.Values(m)

Iterators over keys or values, in random order

maps.Insert(m, seq)

Add the pairs of an iterator to m

maps.Collect(seq)

New map built from an iterator of pairs

Notes:

  • Keys and Values return iterators, not slices. Pass them to slices.Collect, slices.Sorted or a range loop. They are in the same random order as a range over the map.

  • Clone and Copy are shallow: map or slice values (and pointers) are shared between the copies.

  • slices.All produces pairs of (index, value), so maps.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.

  • Equal and EqualFunc are the way to compare maps; == between two maps is a compile error, because a map can only be compared with nil (m == nil). reflect.DeepEqual also 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
...

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.Mutex or, for read-mostly workloads, a sync.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.Map for the two cases it is designed for: keys written once and read many times, or goroutines that touch disjoint sets of keys. It stores any keys and values and has methods such as Load, Store, LoadOrStore, Delete and Range.

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 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).

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 the maps package, the clear builtin and iterator functions, so today you would write maps.Clone, maps.Keys with slices.Sorted and clear(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 maps and slices packages, clear and sync.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

sync.Map

1.12

fmt prints maps in key order

1.21

clear builtin; the maps package (Clone, Copy, Equal, EqualFunc, DeleteFunc)

1.22

Range over integers and per-iteration loop variables (used in the examples)

1.23

maps.Keys, maps.Values, maps.All, maps.Insert, maps.Collect iterator functions

1.24

New built-in map implementation based on Swiss tables

1.25

sync.WaitGroup.Go

Go 1.27 changes nothing in the language rules for maps.