liveBook · Manning

Map

As explained in 100 Go Mistakes and How to Avoid Them

A map is a Go data structure that associates keys with values and requires keys to satisfy comparability rules.

A Go map holds distinct keys and their values without promising an order. It is backed by a hash table: a hash function directs each key to a bucket, where Go looks for the matching key. Not every type can be a key; a slice, for example, cannot. section 3.11.1 section 12.6.2

Size a map before a large insertion

Starting with a small map and adding a million entries makes it grow and redistribute keys. If the expected count is known, write `m := make(map[string]int, 1_000_000)`. The number is a sizing hint, not a maximum: more entries can still be added. This avoids much of the work of creating and reorganizing buckets during insertion. Use the hint when the expected count is reasonably known, rather than treating every map as fixed-size. section 3.11.2

Deletion does not give buckets back

A map can retain substantial memory after its entries are deleted. In a measured `map[int][128]byte`, adding one million entries raised heap use to 461 MB; deleting them and running garbage collection left 293 MB. Deletion clears slots, but the bucket count does not shrink. In the same comparison, `map[int]*[128]byte` left 38 MB after deletion and collection. Pointer values can therefore change the memory trade-off, but deleting entries alone is not a way to shrink the map's buckets. section 3.12

Do not use range as an ordering rule

`for k := range m { fmt.Print(k) }` does not print keys in sorted, insertion, or bucket-storage order. Even two traversals of the same map can differ. The iteration order is unspecified—not a source of uniform randomness—so code that needs an order must not infer one from a map traversal. section 4.4.1

A map assignment is not a protected snapshot

Suppose a balance cache has `balances map[string]float64` guarded by `sync.RWMutex`. Locking only long enough to run `balances := c.balances`, then ranging over `balances` after unlocking, still races with a goroutine updating the cache: the assignment refers to the same buckets. For a short traversal, hold the read lock throughout it. If the traversal should run outside the lock, create another map and copy its entries while locked. A test run with `-race` exposes the faulty version. section 9.10

Two boundary cases are worth checking:

  • For uncertain JSON fields, `json.Unmarshal` can fill a `map[string]any`, but a numeric field such as `"id": 32` becomes `float64`, not `int`. section 10.3.3
  • For a `[]byte` lookup key, use a string key type. A direct lookup such as `m[string(bytes)]` receives a compiler optimization that the shown intermediate `key := string(bytes); m[key]` version does not. section 12.6.2

How 100 Go Mistakes and How to Avoid Them covers it

How deep it goes
Explains hash buckets, growth, and non-shrinkage through mechanisms and measurements rather than stopping at map syntax.
Examples
Uses million-entry heap measurements and a balance-cache race to expose retained buckets and shallow assignments.
What it assumes
Assumes familiarity with Go map syntax and goroutines while explaining hash buckets before using them to diagnose mistakes.
What you can do after
Apply it via `make` and `sync.RWMutex` to pre-size maps and protect shared iteration.
How it connects
Connects maps to hash tables, garbage collection, mutexes, and data races.

Also explained in

Compare how 42 books explain Map →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of 100 Go Mistakes and How to Avoid Them. The section links point to the parts of the book it draws on.