Skip to content

A pointer receiver changes the method set

Level: 201 · anyone who has seen does not implement … (method X has pointer receiver) and wondered why c.X() worked a line earlier

One line: The method set of *T holds every method declared on T or *T; the method set of T holds only those with a value receiver — and c.Increment() on a variable works anyway because the compiler rewrites it as (&c).Increment(), which it cannot do for a map element, a value inside an interface, or a composite literal.

Counter has Value on a value receiver, Increment on a pointer receiver, and Reset on a value receiver by mistake. The program lists both method sets through reflect, calls the methods on a variable, a slice element and a map of pointers, and asks each interface at run time which type satisfies it.

Verified output of a_pointer_receiver_changes_the_method_set_go.go — regenerated by tools/run_examples.py, never hand-typed.

method set of Counter:  [Reset Value]
method set of *Counter: [Increment Reset Value]
after two c.Increment() calls, c.Value() = 2
after inc.Increment() through *Counter, c.Value() = 3 ; the Valuer holds a copy taken earlier: 2
after c.Reset() with a value receiver, c.Value() = 3 (a copy was reset, not c)
counters[0].Increment() on a slice element: counters[0].Value() = 1
byQueue["web"].Increment() through a *Counter in the map: 1
does any(c) hold an Incrementer?   false
does any(&c) hold an Incrementer?  true
does any(c) hold a Valuer?         true

Reading the output

Two method sets. reflect.TypeOf(c) lists [Reset Value]; reflect.TypeOf(&c) lists [Increment Reset Value]. That is the spec's rule ↗ exactly: the method set of a pointer type *T is the set of all methods with receiver *T or T, and the method set of T is the methods with receiver T. Increment is in one and not the other; the two value-receiver methods are in both. (reflect lists exported methods only, sorted by name; that is why the order is alphabetical.)

c.Increment() works on a variable — because c has an address. The spec on calls ↗ says that if x is addressable and &x's method set contains m, then x.m() is shorthand for (&x).m(). Two calls, and c.Value() reads 3 after the third one through the Incrementer: every call reached the same Counter. A slice element is addressable too, so counters[0].Increment() incremented in place.

A value receiver gets a copy. c.Reset() compiled and did nothing to c: its receiver was a copy, the copy was zeroed, and c.Value() still says 3. The Valuer stored earlier holds its own copy as well, taken at the assignment, which is why it still reports 2. Nothing warns about this; it is the first bullet of the vet page's What no tool catches.

The interface answers at run time what the compiler answers at compile time. any(c).(Incrementer) is false and any(&c).(Incrementer) is true: the runtime checks the dynamic type's method set, and Counter's has no Increment. any(c).(Valuer) is true, because Value has a value receiver. Type switches and assertions is about that check.

The map holds pointers on purpose. byQueue["web"].Increment() compiles because the element type is *Counter. Had it been Counter, the call would have been the second error below.

Where the compiler says no

The same four calls with no address to take. The first is the message from the title; the other three are the same refusal in three places where no & can be inserted — a map element, a value held in an interface (even after a type assertion, which yields a value, not a variable), and a composite literal. The second program is the same four lines with the address in hand, and it builds.

Verified output of a_pointer_receiver_not_in_the_method_set_sh.sh — regenerated by tools/run_examples.py, never hand-typed.

$ go build not_addressable.go
# command-line-arguments
./not_addressable.go:12:24: cannot use c (variable of struct type Counter) as Incrementer value in variable declaration: Counter does not implement Incrementer (method Increment has pointer receiver)
./not_addressable.go:14:17: cannot call pointer method Increment on Counter
./not_addressable.go:16:18: cannot call pointer method Increment on Counter
./not_addressable.go:17:12: cannot call pointer method Increment on Counter
exit status 1
$ go build addressable.go
exit status 0

The spec's list of what is addressable ↗ — a variable, a pointer indirection, a slice index, a field selector or array index of an addressable operand, and a composite literal only directly behind & — is the whole rule. A map element is left out because the map may move it; an interface's value is left out because it is a copy the interface owns.

a_pointer_receiver_changes_the_method_set_go.go and a_pointer_receiver_not_in_the_method_set_sh.sh

a_pointer_receiver_changes_the_method_set_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// A method with a pointer receiver belongs to the method set of *T only; a
// method with a value receiver belongs to both T and *T. The compiler writes
// (&c).Increment() for you when c is addressable -- a variable, a slice
// element, a field of one of those -- which is why the pointer method seems
// to be on the value until the value is somewhere it cannot take the address
// of (a map element, a value inside an interface): those are compile errors,
// recorded by a_pointer_receiver_not_in_the_method_set_sh.sh.
//
//  go build a_pointer_receiver_changes_the_method_set_go.go && ./a_pointer_receiver_changes_the_method_set_go
package main

import (
    "fmt"
    "reflect"
    "strings"
)

// Counter counts orders. Value reads a copy; Increment must change the
// original, so its receiver is a pointer. Reset has a value receiver by
// mistake: it resets a copy and leaves the counter as it was.
type Counter struct{ orders int }

func (c Counter) Value() int  { return c.orders }
func (c *Counter) Increment() { c.orders++ }
func (c Counter) Reset()      { c.orders = 0 }

// Incrementer is satisfied by *Counter and not by Counter.
type Incrementer interface{ Increment() }

// Valuer is satisfied by both Counter and *Counter.
type Valuer interface{ Value() int }

// methods lists the exported methods reflect sees in a type's method set,
// in the sorted order reflect keeps them in.
func methods(t reflect.Type) string {
    names := make([]string, 0, t.NumMethod())
    for i := range t.NumMethod() {
        names = append(names, t.Method(i).Name)
    }
    return "[" + strings.Join(names, " ") + "]"
}

func main() {
    var c Counter
    fmt.Println("method set of Counter: ", methods(reflect.TypeOf(c)))
    fmt.Println("method set of *Counter:", methods(reflect.TypeOf(&c)))

    c.Increment() // c is addressable: the compiler calls (&c).Increment()
    c.Increment()
    fmt.Println("after two c.Increment() calls, c.Value() =", c.Value())

    var v Valuer = c         // a Counter value satisfies Valuer: Value has a value receiver
    var inc Incrementer = &c // only *Counter satisfies Incrementer
    inc.Increment()
    fmt.Println("after inc.Increment() through *Counter, c.Value() =", c.Value(), "; the Valuer holds a copy taken earlier:", v.Value())

    c.Reset()
    fmt.Println("after c.Reset() with a value receiver, c.Value() =", c.Value(), "(a copy was reset, not c)")

    // A slice element is addressable, so the pointer method works on it in place.
    counters := []Counter{{}, {}}
    counters[0].Increment()
    fmt.Println("counters[0].Increment() on a slice element: counters[0].Value() =", counters[0].Value())

    // A map element is not addressable -- so a map of pointers is the usual shape.
    byQueue := map[string]*Counter{"web": {}}
    byQueue["web"].Increment()
    fmt.Println("byQueue[\"web\"].Increment() through a *Counter in the map:", byQueue["web"].Value())

    // The same rule, checked at run time on interface values.
    _, ok := any(c).(Incrementer)
    fmt.Println("does any(c) hold an Incrementer?  ", ok)
    _, ok = any(&c).(Incrementer)
    fmt.Println("does any(&c) hold an Incrementer? ", ok)
    _, ok = any(c).(Valuer)
    fmt.Println("does any(c) hold a Valuer?        ", ok)
}

a_pointer_receiver_not_in_the_method_set_sh.sh in full — pasted here by tools/run_examples.py from the file CI runs.

#!/usr/bin/env bash
# Counter has Increment on a pointer receiver only. Four lines that look
# like they should work are compile errors: assigning a Counter value to an
# interface that needs Increment, and calling Increment on a map element, on
# a value held in an interface, and on a composite literal -- none of which
# is addressable. The same four lines with an address in hand compile.
set -u
export GOTOOLCHAIN=local

dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT

cat >"$dir/not_addressable.go" <<'GO'
package main

type Counter struct{ orders int }

func (c Counter) Value() int  { return c.orders }
func (c *Counter) Increment() { c.orders++ }

type Incrementer interface{ Increment() }

func main() {
    var c Counter
    var inc Incrementer = c // Counter's method set has no Increment
    byQueue := map[string]Counter{"web": {}}
    byQueue["web"].Increment() // a map element is not addressable
    var boxed any = c
    boxed.(Counter).Increment() // nor is a value inside an interface
    Counter{}.Increment()       // nor is a composite literal
    _ = inc
}
GO

cat >"$dir/addressable.go" <<'GO'
package main

import "fmt"

type Counter struct{ orders int }

func (c Counter) Value() int  { return c.orders }
func (c *Counter) Increment() { c.orders++ }

type Incrementer interface{ Increment() }

func main() {
    var c Counter
    var inc Incrementer = &c // *Counter has Increment
    byQueue := map[string]*Counter{"web": {}}
    byQueue["web"].Increment() // the map holds pointers
    var boxed any = &c
    boxed.(*Counter).Increment() // the interface holds a pointer
    (&Counter{}).Increment()     // an explicit address
    inc.Increment()
    fmt.Println(c.Value(), byQueue["web"].Value())
}
GO

say() { printf '$ %s\n' "$*"; }

say 'go build not_addressable.go'
(cd "$dir" && go build not_addressable.go) 2>&1
echo "exit status $?"

say 'go build addressable.go'
(cd "$dir" && go build addressable.go) 2>&1
echo "exit status $?"

What to do

  • If any method of a type needs a pointer receiver, give them all pointer receivers. Then T satisfies nothing that *T does not, and no reader has to work out which half of the type an interface sees. The wiki's receiver type guidance ↗ says the same: when in doubt, use a pointer.
  • A method that changes the receiver must have a pointer receiver. Reset above compiled and did nothing. A value receiver is right only for a method that reads — and then it is right for small, immutable values such as time.Time, whose methods all take a value.
  • Store pointers in a map, or replace the whole element. m[k].Increment() will never compile on a value type; m[k] = updated or map[string]*Counter are the two ways out.
  • Read does not implement … (method X has pointer receiver) as "pass &v". The value has the method; the interface was handed the wrong half of the type.

Practice

A Wallet has Deposit(cents int) and Balance() int, and there is a Depositor interface with both. Choose the receivers so that a deposit changes the wallet it is made to, put three wallets in a slice and deposit into each by index, and print the total. Then use reflect to print the method sets of Wallet and *Wallet, and ask any(...) which of the two satisfies Depositor.

Solution

a_pointer_receiver_changes_the_method_set_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// Kata: a Wallet has Deposit and Balance. Choose the receivers so that
// Deposit changes the wallet it is called on, put three wallets in a slice,
// deposit into each by index, print the total, and show with reflect which
// of Wallet and *Wallet has both methods and so satisfies Depositor.
//
//  go build a_pointer_receiver_changes_the_method_set_kata_go.go && ./a_pointer_receiver_changes_the_method_set_kata_go
package main

import (
    "fmt"
    "reflect"
)

type Wallet struct{ cents int }

// Deposit changes the wallet, so its receiver is a pointer.
func (w *Wallet) Deposit(cents int) { w.cents += cents }

// Balance only reads, so a value receiver is enough and both types get it.
func (w Wallet) Balance() int { return w.cents }

type Depositor interface {
    Deposit(cents int)
    Balance() int
}

func names(t reflect.Type) []string {
    out := make([]string, 0, t.NumMethod())
    for i := range t.NumMethod() {
        out = append(out, t.Method(i).Name)
    }
    return out
}

func main() {
    wallets := []Wallet{{100}, {50}, {0}}
    deposits := []int{20, 30, 50}
    for i, d := range deposits {
        wallets[i].Deposit(d) // wallets[i] is addressable: (&wallets[i]).Deposit(d)
    }
    total := 0
    for _, w := range wallets { // w is a copy; Balance reads it, nothing writes it
        total += w.Balance()
    }
    fmt.Println("balances after deposits:", wallets)
    fmt.Println("total:", total)

    fmt.Println("methods of Wallet: ", names(reflect.TypeOf(Wallet{})))
    fmt.Println("methods of *Wallet:", names(reflect.TypeOf(&Wallet{})))
    _, valueOK := any(Wallet{}).(Depositor)
    _, pointerOK := any(&Wallet{}).(Depositor)
    fmt.Println("Wallet satisfies Depositor:", valueOK, "; *Wallet satisfies Depositor:", pointerOK)
}

Verified output of a_pointer_receiver_changes_the_method_set_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.

balances after deposits: [{120} {80} {50}]
total: 250
methods of Wallet:  [Balance]
methods of *Wallet: [Balance Deposit]
Wallet satisfies Depositor: false ; *Wallet satisfies Depositor: true

In other languages

  • The Rust library's Method resolution ↗ — Rust has the same automatic & and &mut on a method call, but the receiver kind is written on the method (&self, &mut self, self) and a &mut self method on a value you cannot borrow mutably is a compile error about the borrow, not about a method set: a trait is implemented for a type or for &T, and the compiler tells the two apart at every call. Its What a trait is ↗ is the counterpart of an interface.
  • C++ member functions are declared const or not (cppreference ↗) — the closest thing to a value receiver is a const member, and a non-const member on a const object is the compile error; Java has neither kind, every method sees the object itself (the Java tutorial ↗).

Sources

  • The Go specification: Method sets ↗, Method declarations ↗, Calls ↗ (the (&x).m() shorthand) and Address operators ↗ (what is addressable).
  • reflect.Type ↗ — NumMethod and Method count exported methods, in lexicographic order.
  • The Go wiki, Go Code Review Comments: Receiver Type ↗.
  • The Go FAQ, Should I define methods on values or pointers? ↗.
  • Alan A. A. Donovan and Brian W. Kernighan, The Go Programming Language (Addison-Wesley, 2015), chapter 6, "Methods" — section 6.2, "Methods with a Pointer Receiver", and chapter 7, "Interfaces" — section 7.3, "Interface Satisfaction".
  • Jon Bodner, Learning Go, 2nd ed. (O'Reilly, 2024), chapter 7, "Types, Methods, and Interfaces" — "Pointer Receivers and Value Receivers".
  • Adam Freeman, Pro Go (Apress, 2022), chapter 11, "Using Methods and Interfaces" — "Understanding Pointer and Value Receivers" and "Understanding the Effect of Pointer Method Receivers".