Skip to content
Jared's Notes Jared's Notes
Go back

TIL: Go Map Elements Aren't Addressable

In Go, map values are not addressable. This compiles fine for a slice but not for a map:

s := []int{1, 2, 3}
p := &s[0] // fine

m := map[string]int{"a": 1}
p := &m["a"] // compile error: cannot take address of m["a"]

The same rule bites you with structs stored by value in a map: you can’t assign directly to a field:

type Point struct{ X, Y int }

m := map[string]Point{"a": {1, 2}}
m["a"].X = 5 // compile error: cannot assign to struct field m["a"].X in map

The workaround is to read the value out, mutate the copy, and write it back:

p := m["a"]
p.X = 5
m["a"] = p

or store pointers in the map (map[string]*Point) if you need to mutate in place.

Why?

A Go map is a hash table that grows incrementally. When it crosses its load factor, the runtime allocates a larger set of buckets and evacuates existing entries into them and the values live inside those buckets. So the map is allowed to internally copy and move around values, which would invalidate pointers into the map.

m := map[int]int{0: 3}

p := backingStore(m) // pretend: p ≈ &m[0] = 0xc000012068, a pointer to 3's memory

for i := 1; i < 1000; i++ {
	m[i] = i // somewhere in here the map crosses its load factor and grows
}

q := backingStore(m) // 3's memory location has changed to 0xc000012090

This is enforced statically, not dynamically. The compiler rejects &m[k] for every map, even one you can see will never grow past its initial size, because it has no way to prove that at compile time, so the restriction applies uniformly to all map index expressions.

The check itself is just a mode comparison in the type checker. A map index expression never gets mode() == variable, so the & operator rejects it, before the compiler has generated any code or the program has run at all.

How this differs from slices and arrays

Slices and arrays are addressable because their elements sit in a backing array that is stable storage. For a slice of a struct type, &s[0], s[0].Inc(), and s[0].N = 7 all compile.

Arrays: have a fixed size and never grow, so an element’s address is always valid and keeps pointing at the same, live memory for the array’s lifetime.

Slices: are also addressable, pointing at a real backing array. The one caveat is append: when it exceeds capacity, it allocates a new backing array and copies the elements over.

A pointer taken before that reallocation stays valid; it just keeps referencing the old array, which the GC keeps alive as long as the pointer exists. So it’s not dangling; it’s simply detached from the slice, now pointing at a stale copy rather than the slice’s current element.

References


Share this post:

Next Post
C# Async / Await State Machine