skip to content

In Go, why does the method value f := c.Total go stale when Total has a value receiver?

level: middleimportance: should knowfreq 50%

answer

  1. the photograph, not the window
  2. two rules stacked: eager binding, receiver copy
  3. the copy is made once, at binding
  4. a pointer receiver saves an address instead
  5. no call-site trick rescues a value receiver

basics

~20 s

Because c.Total is evaluated immediately and a value receiver saves a copy of c beside the function. Later writes go to the original variable, which the saved copy is not connected to, so f keeps returning old values.

solid answer

~40 s

The expression `c.Total` is evaluated where it appears, not where `f` is eventually called. Since `Total` is declared with a value receiver, Go copies the whole `c` struct at that moment and stores the copy with the function value; the method then runs against that copy forever. Writing `c.n = 42` afterwards mutates the original variable, which the saved copy has no link to, so `f()` still reports the old count. Declaring `func (c *Counter) Total() int` changes it: `c.Total` becomes `(&c).Total`, the saved receiver is a pointer, and the copy is only of the pointer — so the method reads whatever the struct holds at call time. Note there is no way to make a value-receiver method value follow updates; the receiver form is the fix, not the call site.

code

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

func (c Counter) Total() int { return c.n }

func (c *Counter) Live() int { return c.n }

func demo() {
	c := Counter{n: 1}
	f := c.Total // saves a copy of c
	g := c.Live  // saves &c
	c.n = 42
	fmt.Println(f()) // 1
	fmt.Println(g()) // 42
}

go deeper

for a junior

Know the outcome first: taking a method value from a value-receiver method copies the struct then and there, so later edits to the variable are not seen. Be able to predict what the printed number is.

for a middle

Explain it as two rules combining — the receiver expression is evaluated once at binding, and a value receiver always operates on a copy — and show that switching to a pointer receiver saves an address instead.

for a senior

Be ready to say how you would detect this in a codebase: nothing static flags it, so you describe the test shape that mutates the receiver between binding and calling, and the receiver-form convention you would enforce in review.

for a principal

The lever you own is consistency: pick one receiver form per type and hold the line, because a type with mixed receivers turns every method value handed across a package boundary into a question the caller cannot answer from the signature.

## Two independent facts, combined The surprise comes from stacking two rules that are individually unremarkable. **Fact one: binding is eager.** `c.Total` is an expression. Go evaluates the receiver operand `c` when it evaluates that expression and saves the result with the resulting function value. Nothing is re-evaluated at call time. **Fact two: a value receiver takes a copy.** Any call to a value-receiver method operates on a copy of the receiver. For a normal call the copy is made per call, and its short life is invisible. For a method value the copy is made **once, at binding**, and then survives for as long as the function value does. Put together: `f := c.Total` snapshots the struct. ```go type Counter struct{ n int } func (c Counter) Total() int { return c.n } c := Counter{n: 1} f := c.Total c.n = 42 fmt.Println(f()) // 1 ``` The last line is the whole lesson: `f` is not a view onto `c`, it is a frozen photograph of `c`. ## What a pointer receiver changes ```go func (c *Counter) Live() int { return c.n } c := Counter{n: 1} g := c.Live // shorthand for (&c).Live c.n = 42 fmt.Println(g()) // 42 ``` With a pointer receiver, the saved receiver is `&c`. A copy is still made — of the **pointer** — but the struct it points at is shared, so the method reads the current fields. This is the only reliable fix. Wrapping the value-receiver form differently does not help: `(&c).Total` is still `(*(&c)).Total` for a value-receiver method, so it copies just the same. If the method promises to observe live state, its receiver has to be a pointer. The pointer form has its own subtlety. What is frozen is now the pointer, not the pointee. If you bind `g := p.Live` from a `*Counter` variable `p` and later reassign `p = other`, `g` still calls into the original struct. ## Cost, not just correctness Because the copy happens at binding rather than per call, taking a method value from a large value-receiver struct copies that struct once and keeps it alive for the lifetime of the function value. If you are stashing many such method values in a registry, you are stashing that many struct copies. This is usually a rounding error, but it is the honest answer when an interviewer asks whether the construct costs anything. ## How you catch it This bug type is invisible to the compiler and to `go vet` — the code is entirely legal, and both receiver forms compile. The test that catches it is a table test written in the shape of the failure: build the method value, mutate the receiver's fields, then call and assert the *new* value is observed. A test that binds and immediately calls will pass under both receiver forms and tells you nothing. ```go for _, tc := range cases { c := Counter{n: tc.start} f := c.Live c.n = tc.updated if got := f(); got != tc.updated { t.Errorf("%s: method value saw %d, want %d", tc.name, got, tc.updated) } } ``` ## The mental model to state out loud A method value is one small object holding a saved receiver plus a function to run against it. "Saved receiver" is the phrase that does the work: for a value receiver the saved thing is a struct copy, for a pointer receiver it is an address. Everything else — whether updates are visible, whether a nil pointer panics at bind time or at call time, whether the receiver has to be addressable — follows from asking what exactly was saved.

  • Can you keep the value receiver and still get live updates by writing (&c).Total instead?
    No. For a value-receiver method, `(&c).Total` is shorthand for `(*(&c)).Total`, so Go dereferences and copies exactly as before. The receiver declaration decides it; there is no call-site spelling that turns a value-receiver method value into a live view.
  • With a pointer receiver, is anything still frozen at binding time?
    Yes, the pointer itself. If you bind from a `*Counter` variable and then reassign that variable to a different struct, the method value keeps calling into the original one. The pointee's fields are live; which pointee you got is not.
  • Would go vet or the compiler warn you about the value-receiver version?
    Neither. Both receiver forms are legal and the copy is intentional language behaviour, so there is nothing to diagnose statically. The only reliable detection is a test that mutates the receiver between binding and calling, then asserts the new value is observed.

saying these in an interview costs you the question

  • Says the copy is taken on each call rather than at binding
  • Claims taking the address at the call site fixes a value receiver
  • Confuses this with the receiver being nil
  • Thinks the compiler or go vet flags the stale binding
  • Believes a pointer receiver makes everything about the binding live