skip to content

Why can't you call a pointer-receiver method on a Go map element or on a function's return value?

level: middleimportance: should knowfreq 60%

answer

  1. the compiler needs an address
  2. not every expression has one
  3. slice element yes, map element no
  4. the map may move entries as it grows
  5. read it out, change it, put it back

basics

~20 s

Calling a pointer-receiver method on a value makes the compiler take that value's address, and only addressable operands have one. Map index expressions and call results are not addressable, so the call is rejected; variables and slice elements are.

solid answer

~40 s

The shorthand `x.M()` for a method declared with receiver `*T` is really `(&x).M()`, and the compiler will only write that when `x` is **addressable**. Variables, pointer dereferences, slice index expressions and fields of addressable structs are addressable; map index expressions, function and method call results, and type-assertion results are not. So `recent[0].inc()` compiles on a `[]stats` while `byName["file"].inc()` on a `map[string]stats` does not, and the same rule is why `byName["file"].hits++` fails. Map elements are excluded because the map is free to move entries as it grows, so no stable address exists. The two workarounds are to copy the value out, mutate it and assign it back, or to declare the map as `map[string]*stats` so the element already is a pointer.

code

go · 21 lines
go
type stats struct {
	hits int
}

func (s *stats) inc() {
	s.hits++
}

func record(byName map[string]stats, recent []stats) {
	// a slice element is addressable, so this means (&recent[0]).inc()
	recent[0].inc()

	// both rejected: a map element has no address to take
	// byName["file"].inc()
	// byName["file"].hits++

	// the workaround: read it out, change the copy, write it back
	s := byName["file"]
	s.inc()
	byName["file"] = s
}

go deeper

for a junior

Know the symptom and the fix: if a compile error appears on a map lookup you are trying to change, copy the value into a variable, change that, and assign it back into the map.

for a middle

Explain the mechanics - the pointer-receiver call is shorthand for taking an address, and the specification lists which operands have one. Be able to say why a map element is excluded and a slice element is not.

for a senior

Turn it into a data-shape decision: whether the map holds values or pointers changes copy cost, aliasing, zero-value behaviour and what a missing key means. Say which you would choose for a mutable registry and why.

for a principal

Watch the ripple: exposing a map of values versus a map of pointers in a package API fixes the mutation semantics every consumer gets, and it is not a change you can make quietly later.

## Two rules that combine This behaviour comes from two separate rules meeting. **Rule one - the call-site shorthand.** If a method is declared with receiver `*T` and you write `x.M()` where `x` has type `T`, the compiler rewrites the call as `(&x).M()`. The shorthand is only legal when `x` is addressable. **Rule two - what is addressable.** Go's specification lists the addressable operands: - a variable; - a pointer indirection, `*p`; - a slice index expression, `s[i]` - always, because the element lives in a backing array; - an array index expression, `a[i]`, when the array itself is addressable; - a field selector `x.f` when `x` is addressable; - plus one special case: you may take the address of a composite literal, `&T{}`. Everything else is not addressable. The ones that matter in practice: - a **map index expression**, `m[k]`; - a **function or method call result**, `f()`; - a **type assertion result**, `v.(T)`; - a **string index expression**, `s[i]`, and constants and most literals. Put the two rules together and `m[k].inc()` fails while `s[0].inc()` succeeds, even though the two look identical. ## Why a map element has no address This is a design decision with an implementation reason behind it. A Go map grows and rearranges its storage as entries are added; the memory holding a given value may move. If the language handed out `&m[k]`, that pointer could be left pointing at storage the map has abandoned, and a write through it would be silently lost. Rather than define what happens, the specification simply makes map elements non-addressable, which pushes the problem to compile time. A slice element does not have this problem *for the duration of the expression*: the element lives in a backing array and `&s[i]` is a well-defined pointer into it. (Appending later may allocate a new array and leave that pointer aimed at the old one - a real hazard, but a different one, and not what the addressability rule is about.) ## The symptoms you will actually see Three failures, one cause: ```go byName["file"].inc() // pointer-receiver method on a map element byName["file"].hits++ // increment is an assignment to a map element's field byName["file"].hits = 1 // assignment to a struct field in a map ``` The last two report that you cannot assign to a struct field of a map element. The first reports that the method cannot be called because the operand is not addressable. Both are the same rule wearing different clothes: an assignment target must be addressable too. Note what still works: **reading** is fine. `n := byName["file"].hits` compiles, because reading a field only needs the value, not its address. And a value-receiver method call, `byName["file"].report()`, also compiles - it receives a copy, and copying needs no address. ## The two fixes **Copy out, mutate, put back.** ```go s := byName["file"] s.hits++ byName["file"] = s ``` This is explicit and correct, and it makes the copy obvious to a reader. It costs a copy of the struct in each direction, which is irrelevant for a small struct and worth thinking about for a large one. It is also not atomic - if two goroutines do this concurrently you have a lost update *and* a data race, but concurrent map writes are unsafe regardless. **Store pointers.** Declaring the map as `map[string]*stats` makes `byName["file"]` a pointer value, and calling a pointer-receiver method on a pointer needs no address-taking at all. Mutations are then visible to everyone holding the pointer, which may be exactly what you want for a registry of long-lived objects - or exactly what you do not want if you were relying on map values behaving like independent copies. It also means a missing key yields a nil pointer rather than a usable zero value, so lookups need an `ok` check before use. ## The call-result case ```go newStats().inc() // rejected ``` A function's return value is a temporary with no home; the language will not invent one. If `inc` has a pointer receiver, either assign the result to a variable first - `s := newStats(); s.inc()` - or have the constructor return `*stats` in the first place, which is the usual shape for a type with pointer-receiver methods. ## How to recognise it in review When someone reports "the compiler will not let me change this", ask what is on the left of the dot. If it is a map lookup, a function call or a type assertion, the answer is addressability and not anything about the method. The fix is mechanical; the design question underneath - should this map hold values or pointers - is the part worth discussing.

  • Why is a slice element addressable when a map element is not?
    A slice element lives at a known offset in a backing array, so `&s[i]` is a well-defined pointer. A map may rehash and move entries as it grows, so an address into it could be left pointing at abandoned storage. Rather than define that hazard, the specification makes map index expressions non-addressable and the compiler rejects the code.
  • Reading `byName["file"].hits` compiles but writing it does not. Why the difference?
    Reading a field only needs the value, and copying a map element out is always allowed. Writing needs an addressable target so the compiler can store into it, and a map element is not addressable. The same asymmetry explains why a value-receiver method call on a map element compiles while a pointer-receiver one does not.
  • What do you give up by declaring the map as map[string]*stats to dodge the problem?
    Elements stop being independent copies: anyone holding the pointer sees every mutation, which is either the point or a bug depending on your intent. A missing key now yields a nil pointer instead of a usable zero value, so lookups need an `ok` check, and the values are separately allocated rather than living inside the map's storage.
  • Which other operands are not addressable in Go?
    A function or method call result, a type-assertion result, a string index expression, constants and most literals. The notable exception cutting the other way is that you may take the address of a composite literal, which is why `&stats{}` is legal even though `stats{}` is not a variable.

saying these in an interview costs you the question

  • Says map elements are read-only
  • Claims the method itself is the problem
  • Thinks &m[k] is legal and just discouraged
  • Believes slice and map elements behave identically
  • Suggests a type conversion as the fix
  • Assumes reading a map element's field also fails