In Go, why does the method value f := c.Total go stale when Total has a value receiver?
answer
- the photograph, not the window
- two rules stacked: eager binding, receiver copy
- the copy is made once, at binding
- a pointer receiver saves an address instead
- no call-site trick rescues a value receiver
basics
~20 sBecause 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 sThe 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 linestype 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
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.
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.
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.
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