skip to content

Why is a Go map element unaddressable, making `&m[k]` and `m[k].n++` compile errors?

level: middleimportance: should knowfreq 46%

answer

  1. growth may move the entry
  2. an address you hold could be abandoned
  3. compare with &s[i] on a slice
  4. copy out, change it, assign back
  5. or make the element type a pointer

basics

~20 s

Inserting into a map can move existing entries to new storage, so a pointer into a map could be left dangling. Go therefore makes map elements unaddressable. Copy the value into a variable, change it, assign it back, or store pointers.

solid answer

~50 s

A map element lives in a slot the runtime owns, and the runtime is free to move it: when the map grows, entries are rehashed into freshly allocated storage. If `&m[k]` were legal, that pointer would quietly refer to abandoned memory after the next insert, so the language forbids taking the address at all. That is why `m[k].n++` does not compile when the element type is a struct — assigning to a field needs the address of the struct — and why you cannot call a pointer-receiver method on `m[k]`. Two fixes: read the whole value out, change it, write it back (`v := m[k]; v.n++; m[k] = v`), or declare the map as `map[K]*V` so the element is a pointer and `*m[k]` is addressable. Note that `m[k]++` on a `map[string]int` is fine — that assigns the element itself, not a field of it.

code

go · 14 lines
go
type counter struct{ n int }

m := map[string]counter{}
// m["a"].n++ // compile error: cannot assign to struct field m["a"].n in map

v := m["a"] // copy out; a missing key yields the zero value
v.n++
m["a"] = v // write the whole element back

p := map[string]*counter{"a": {}}
p["a"].n++ // fine: the element is a pointer, so *p["a"] is addressable

totals := map[string]int{}
totals["a"]++ // also fine: this assigns the element itself, not a field of it

go deeper

for a junior

Recall the workaround: read the element into a variable, change it, assign it back. Recognise the compiler message about assigning to a struct field in a map when you hit it.

for a middle

Explain the mechanism — inserting can rehash entries into new storage, so any address into a map could be abandoned — and contrast it with a slice, whose backing array is never moved under an existing pointer.

for a senior

Weigh the fixes in real code: copy-out costs a value copy per update, map[K]*V costs an allocation per key and more pointers for the collector to scan. Say which you would choose for a hot counter map with millions of keys, and why.

for a principal

Own it as an API question: whether a package hands out map values, pointers into its state, or accessor methods decides what callers may mutate, and that shape is very hard to change once other teams depend on it.

## Addressability, briefly In Go, only certain expressions are *addressable* — that is, you may apply `&` to them, assign to a field or index inside them, or use them as the receiver of a pointer-receiver method call. The addressable expressions are variables, pointer dereferences, slice index expressions, and field selectors and array index expressions of addressable operands. Two notable things are **not** addressable: the result of a function call, and **a map index expression**. `&m[k]` is a compile-time error, full stop. ## Why the language forbids it A map's storage is owned and rearranged by the runtime. When the map crosses its load threshold, it allocates new storage and rehashes entries into it; an entry that was at one address is now at another. Deleting can also mark and reuse slots. If `&m[k]` were legal, the pointer you obtained would refer to a slot the map may abandon on the very next insert. You would be writing into memory the map no longer consults — not a crash, which would at least be loud, but a silent lost update. Go's answer is to make the mistake impossible to express rather than to document it. Contrast a slice. `&s[i]` *is* legal, and the reason is instructive: a slice's backing array is never moved in place. `append` may allocate a **new** array and copy into it, but the old array stays exactly where it is and stays alive as long as your pointer refers to it. Your pointer never dangles; you simply stop observing writes made through the reslice. A map has no such stable object behind it that the caller can hold. ## What breaks, concretely Given `type stats struct{ n int }` and `m := map[string]stats{}`: - `&m["a"]` — compile error, cannot take the address of a map element. - `m["a"].n++` — compile error: *cannot assign to struct field m["a"].n in map*. Incrementing the field requires the struct's address. - `m["a"].Incr()` where `Incr` has receiver `*stats` — compile error, because the call would need `&m["a"]`. What still works, and confuses people who over-generalise the rule: - `m["a"] = stats{n: 1}` — assigning the whole element is exactly what the map API supports. - `counts["a"]++` on a `map[string]int` — legal, because this assigns *the element*, not a field inside it. The compiler rewrites it to a read, an add, and a store. - `m["a"].Read()` where `Read` has a **value** receiver — legal, since a copy suffices. ## The two fixes, and how to choose **Copy out, mutate, assign back.** ```go v := m[key] // a missing key yields the zero value, which is usually what you want v.n++ m[key] = v ``` This costs one copy of the struct in and one out, and no allocation. For a small struct — a few ints, a couple of strings — this is the cheaper option and reads clearly. It is also what a counter map wants: the zero value of the struct doubles as "not seen yet", so there is no need to check for presence. **Store pointers: `map[K]*V`.** ```go v, ok := m[key] if !ok { v = &stats{} m[key] = v } v.n++ ``` Now the element is a pointer, `*m[key]` is addressable, and `m[key].n++` compiles because it dereferences a pointer rather than indexing a map. The costs are real: one heap allocation per distinct key, an extra indirection on every access, and one more pointer per entry for the garbage collector to scan — which matters when the map holds millions of entries. It also introduces sharing: two places holding the same pointer see each other's mutations, which is either the point or a bug depending on your design. **A third option** for hot paths: keep the values in a slice and let the map hold indexes (`map[K]int`). Slice elements *are* addressable, so `&values[m[k]]` is legal, and the values sit contiguously. This is the pattern that shows up in allocation-sensitive code, at the cost of manual bookkeeping when entries are removed. ## The interview point The wrong answer is "Go just doesn't allow it". The right answer names the mechanism: **map storage may be reallocated and entries moved, so no stable address exists to hand out** — and then contrasts it with slices, where the backing array is stable and `&s[i]` is fine. Getting that contrast right shows you understand what each data structure actually owns.

  • If append can reallocate, why is `&s[i]` on a slice legal?
    Because append allocates a new backing array and leaves the old one untouched. Your pointer keeps referring into the old array, which stays alive and valid as long as the pointer does — you just stop seeing later writes made through the reslice. Nothing dangles, so the language permits it.
  • Copy-out-and-assign-back or map[K]*V — which do you pick?
    For small values, copy out and assign back: no allocation, no extra pointer for the collector to scan. For large structs, or when several holders must observe the same mutation, use map[K]*V and accept one allocation per key plus an indirection on every access.
  • Why does `counts[k]++` compile on a map[string]int?
    Because that assigns the map element itself, which the map API supports — the compiler turns it into a read, an add and a store. What fails is assigning to something *inside* a non-addressable element, such as a field of a struct value, which would require the element's address.
  • Can you call a pointer-receiver method on a map element?
    Not directly: the call needs the receiver's address and a map element has none, so it does not compile. Assign the element to a local variable, which is addressable, and call the method there, or store pointers in the map.

The map is free to rearrange its shelves whenever it gets crowded, so it refuses to give you the address of a shelf it may abandon on the next insert.

saying these in an interview costs you the question

  • Says `&m[k]` would be fine as long as the map never grows
  • Calls `m[k].n++` a runtime panic instead of a compile error
  • Thinks the restriction exists because map order is random
  • Claims map[K]*V is always the better design
  • Generalises the rule to slices, where &s[i] is legal