skip to content

Why can't you assign to a struct field of a Go map element, as in m[k].Count++?

level: middleimportance: should knowfreq 50%

answer

  1. m[k] is a value, not a variable
  2. one word: addressability
  3. growth may relocate entries
  4. whole-element assignment is still legal

basics

~20 s

Map elements are not addressable in Go, so you cannot take &m[k], assign to a field of an element, or call a pointer-receiver method on one. Read the value into a variable, modify that copy, and assign the whole element back — or make the value type a pointer.

solid answer

~50 s

`m[k]` is not a variable, it is a value produced by a lookup, and the language says map elements are not addressable. Because `m[k].Count++` needs the address of the element to write part of it, the compiler rejects it with `cannot assign to struct field m[k].Count in map`; `&m[k]` and calling a pointer-receiver method on `m[k]` fail for the same reason. Assigning the *whole* element is fine, so `m[k] = v` and `m[k]++` on a `map[string]int` both compile — `m[k]++` is defined as assigning the element, not as writing part of it. The two fixes are read-modify-write (`v := m[k]; v.Count++; m[k] = v`) or making the value type `*T`, after which `m[k].Count++` compiles because you are writing through a pointer. Slice and array elements *are* addressable, which is why `&s[i]` and `s[i].Count++` are perfectly legal.

code

go · 10 lines
go
type stat struct{ Hits int }

m := map[string]stat{"go": {}}

// m["go"].Hits++   // cannot assign to struct field m["go"].Hits in map
// p := &m["go"]    // cannot take address of m["go"]

s := m["go"] // copy out (zero value if the key is absent)
s.Hits++
m["go"] = s  // assign the whole element back

go deeper

for a junior

Recognise the compiler error "cannot assign to struct field ... in map" and know the copy-out, mutate, assign-back fix. Remember that assigning the whole element is always allowed.

for a middle

Explain addressability and why a map cannot offer it: entries may be relocated as the table grows, so any address into it could go stale. Contrast with slice elements, which are addressable.

for a senior

Weigh the two designs in real code: struct values with read-modify-write are simple and copy-safe, pointer values allow in-place mutation but introduce nil elements and shared aliases. Spot the pointer-receiver-on-value-map smell in review.

for a principal

Treat the map's value type as part of the contract. Exposing map[string]*T lets every caller mutate shared state you no longer control; exposing map[string]T forces copies but keeps ownership clear.

## Addressability, in one paragraph Go divides expressions into those that designate a storage location — variables, pointer dereferences, struct fields of addressable operands, slice elements — and those that merely produce a value. Only the first kind is *addressable*: you may apply `&` to it, assign to it, assign to a part of it, and have the compiler implicitly take its address when you call a pointer-receiver method. **Map elements are explicitly excluded from that set.** So for `m` of type `map[string]stat` where `stat` is a struct with a field `Hits`: ```go m["go"].Hits++ // compile error: cannot assign to struct field m["go"].Hits in map p := &m["go"] // compile error: invalid operation: cannot take address of m["go"] m["go"].Inc() // compile error, if Inc has a pointer receiver ``` All three are the same error wearing different clothes: each needs the address of the element. ## Why the language forbids it A map is a hash table that the runtime is free to reorganise. Inserting an entry can force the table to grow, and growing moves existing entries to new storage. If `&m[k]` were legal, that pointer would silently become a pointer to abandoned storage the moment some unrelated insert triggered growth — writes through it would vanish, and the garbage collector would be keeping stale storage alive. Rather than define when such a pointer stays valid (which would constrain every future implementation of maps), Go removes the possibility. Slices do not have this problem: indexing a slice never moves its backing array, and the operation that *can* replace the array, `append`, hands you a new slice header explicitly. ## What is still allowed The restriction is on writing *part* of an element, not on writing the element: ```go m["go"] = stat{Hits: 1} // fine: assign the whole element counts["go"]++ // fine: shorthand for counts["go"] = counts["go"] + 1 counts["go"] += 3 // fine, same reason _ = m["go"].Hits // fine: reading a field of a copy needs no address ``` That last pair surprises people who over-generalise the rule to "you cannot modify map elements at all". You can, wholesale. ## The two fixes **Read, modify, write back.** Copy the element into a variable — which *is* addressable — mutate the copy, and store it back: ```go s := m["go"] // s is a copy; if the key is missing this is the zero stat s.Hits++ m["go"] = s ``` This is explicit, works with a missing key (you get the zero value and then insert), and costs one copy of the struct in each direction. For small structs that is nothing. **Make the value a pointer.** With `map[string]*stat`, the element is a pointer value; `m["go"].Hits++` compiles because it dereferences that pointer, and the struct it points at lives in ordinary heap storage that the map never moves. The trade is that you must now allocate each value, a missing key gives you a nil pointer and the same expression panics, and every holder of the pointer shares the mutation — which is sometimes exactly what you want and sometimes an aliasing bug. A third option that is often the best one for counters: pick a value type where whole-element assignment is enough, such as `map[string]int`, and let `m[k]++` do the work. ## The related diagnostics The compiler messages are precise and worth recognising on sight: `cannot assign to struct field m[k].F in map` for the field write, `cannot take address of m[k]` for `&`, and `cannot call pointer method Inc on stat` for the method case. All three point at the same rule. The method-set version is the one that bites in real code. If `Inc` is declared on `*stat`, then `m["go"].Inc()` fails while `s := m["go"]; s.Inc()` compiles — and the second one silently mutates a copy that is then thrown away, which is a *worse* outcome than the compile error, because it looks like it works. When you see a value-typed map of structs with pointer-receiver methods, that is a design smell: either the methods should take value receivers and return a new value, or the map should hold pointers. ## What interviewers listen for The words "not addressable", the reason (entries may be relocated as the map grows), the fact that whole-element assignment and `m[k]++` remain legal, and at least one correct fix. Candidates who claim maps are read-only, or who propose `&m[k]`, have not internalised the rule.

  • Why are slice elements addressable when map elements are not?
    A slice indexes into a fixed backing array, and indexing never moves it, so `s[i]` designates a real storage location and `&s[i]` stays valid. The operation that can replace the array, `append`, returns a new slice header explicitly instead of relocating behind your back. A map may reorganise its storage on any insert, so no such guarantee exists.
  • Why is m[k]++ legal on a map[string]int if map elements cannot be written in part?
    Because `m[k]++` is defined as `m[k] = m[k] + 1`: a read of the whole element followed by an assignment of the whole element. Neither step needs the element's address. The restriction only forbids writing through a part of it, such as a struct field, or taking a pointer to it.
  • What goes wrong if a map holds struct values whose methods have pointer receivers?
    Calling one directly on `m[k]` is a compile error, because the compiler cannot take the element's address. Worse, copying out first — `s := m[k]; s.Inc()` — compiles and mutates a copy that is discarded, so the code looks correct and silently does nothing. Store `*T` values, or give the methods value receivers.

A map element is a book the librarian may reshelve at any moment. You can borrow a copy and hand back a replacement, but nobody will give you a permanent pointer to the shelf slot it currently occupies.

saying these in an interview costs you the question

  • Claims map elements are read-only and cannot be changed at all
  • Thinks m[k]++ on a map[string]int is also illegal
  • Proposes taking &m[k] as the fix
  • Blames a nil map instead of addressability
  • Copies the struct out, mutates it, and forgets to assign it back