Method Sets

The method set of a type is the set of methods that may be called on an operand of that type, and — crucially — the set of methods the type is checked against when deciding whether it satisfies an interface (Go spec, Method sets). The single most important and most misunderstood rule is asymmetric: the method set of a value type T contains only methods declared with a value receiver func (t T), while the method set of the pointer type *T contains methods declared with either a value receiver or a pointer receiver func (t *T). Because *T’s method set is a superset of T’s, a *T always satisfies every interface that T satisfies, but the reverse is not true. This asymmetry, combined with the rule that automatic address-taking for a pointer-receiver call requires the receiver to be addressable, is the root cause of a large family of “X does not implement Y” compile errors.

Mental Model

Think of a method declared with a pointer receiver as a method that needs a mailing address — it must be able to mutate the value behind the receiver, so it can only operate on something the compiler can hand a pointer to. A method declared with a value receiver only ever sees a copy, so it can be invoked on anything: a copy is always available.

From this, the method-set rule falls out naturally. Given a value v of type T:

  • Calling a value-receiver method on v copies v into the receiver. Always possible.
  • Calling a pointer-receiver method on v requires &v. Possible only if v is addressable — if the compiler can locate it in memory. A pointer p of type *T already is an address, so a pointer-receiver method is always callable through it.

The method set is the static answer to “what does this type promise to an interface.” Interface satisfaction is checked at compile time against the method set, and the method set is computed by the asymmetric rule below — it does not consult addressability. So even though you can write v.PointerMethod() directly on an addressable variable, the value type T still does not have PointerMethod in its method set, and T therefore does not satisfy an interface requiring it.

flowchart TD
    subgraph T["Method set of T (value type)"]
        VT["value-receiver methods: func (t T) ..."]
    end
    subgraph PT["Method set of *T (pointer type)"]
        VP["value-receiver methods: func (t T) ..."]
        PP["pointer-receiver methods: func (t *T) ..."]
    end
    T -- "subset of" --> PT
    VT -.->|"same methods"| VP
    PP -.->|"extra: only in *T"| PP

Diagram: the method set of *T strictly contains the method set of T. Value-receiver methods appear in both; pointer-receiver methods appear only in *T. The insight: whenever you must satisfy an interface that includes any pointer-receiver method, you must use a *T, never a T.

Mechanical Walk-through

The four cases the spec defines

The Go specification’s Method sets section enumerates the method set for every category of type:

  1. A defined (named) type T. Its method set is all methods declared with receiver type T — that is, only value-receiver methods. Pointer-receiver methods are excluded.
  2. A pointer *T, where T is neither a pointer nor an interface. Its method set is all methods declared with receiver *T or T — both pointer-receiver and value-receiver methods.
  3. An interface type. Its method set is the intersection of the method sets of every type in its type set — in practice, just the methods the interface declares. (See Type Sets and Constraints for what “type set” means.)
  4. Any other type (a plain int, a struct literal type, a slice type with no methods) has an empty method set. Structs and pointers to structs with embedded fields gain promoted methods — see Struct Embedding.

Note carefully what case 1 says and does not say. T’s method set is exactly the value-receiver methods. There is no clause “…plus pointer-receiver methods if T is addressable.” Addressability is not a property of a type; it is a property of an expression (an operand). The method set is computed purely from the type.

Why *T has the larger method set

A pointer-receiver method func (t *T) M() receives t as a *T. To call it, the compiler must produce a *T to bind to t. If you already hold a *T, that pointer is the argument — no work needed. If you hold a T value, the compiler must take its address. It can do that only when the value is addressable.

The Go designers chose to make the type-level rule simple and conservative: rather than say “T has pointer-receiver methods sometimes,” they say “T never has them; *T always does.” The convenience of calling v.M() on an addressable v for a pointer-receiver M is then layered on as a syntactic shorthand in the Calls section, not as a change to the method set. This keeps interface satisfaction — which is purely type-level and has no notion of addressability — decidable and predictable.

The addressable-receiver shorthand

The Calls section of the spec states the shorthand precisely, and verbatim: “A method call x.m() is valid if the method set of (the type of) x contains m and the argument list can be assigned to the parameter list of m. If x is addressable and &x’s method set contains m, x.m() is shorthand for (&x).m().” That second sentence is the entire source of the “I can call a pointer method on a value” convenience — and notice it is gated on x being addressable, with no fallback for the non-addressable case. The mirror-image direction (calling a value-receiver method through a pointer, p.m()) needs no special clause: *T’s method set already contains every value-receiver method per case 2, so p.m() binds *p to the value receiver directly.

The operative restriction is the word addressable. The Address operators section defines it verbatim: “The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array.” (A composite literal is the one explicit exception the spec carves out for the & operator, but that exception does not extend the method-call rewrite to literals stored in variables.) What is not addressable, and therefore cannot receive the (&x).m() rewrite, includes:

  • Map index expressions. m["key"] is not addressable, so you cannot call a pointer-receiver method on a map element directly.
  • Return values of function calls. f().M() for a pointer-receiver M fails — the result of f() is a temporary, not addressable.
  • Values stored in an interface. Once a T is boxed in an interface, you can extract a copy but not address the boxed storage.
  • Constants and literals.

So v.PointerMethod() works when v is a local variable, but getThing().PointerMethod() does not.

How this drives interface satisfaction

Interface satisfaction is decided by comparing method sets, with no addressability involved. Consider:

type Stringer interface{ String() string }
type Counter struct{ n int }
func (c *Counter) String() string { return fmt.Sprint(c.n) }

String has a pointer receiver, so it is in the method set of *Counter but not Counter. Therefore *Counter satisfies Stringer; Counter does not. var s Stringer = Counter{} is a compile error; var s Stringer = &Counter{} compiles. The error message is the familiar Counter does not implement Stringer (method String has pointer receiver).

The reason the assignment to an interface is stricter than a direct call is exactly the addressability gap. A direct call Counter{}.String() would also fail (a composite literal is not addressable), but var c Counter; c.String() succeeds because c is addressable. An interface, however, stores a copy of whatever you assign, and that copy lives in interface-managed storage that is never addressable — so the language cannot guarantee a & is available, and it conservatively refuses the assignment at the type level by excluding pointer-receiver methods from T’s method set.

Code Examples

Asymmetry of the two method sets

package main
 
import "fmt"
 
type T struct{ name string }
 
func (t T) Value() string  { return "value:" + t.name }   // value receiver
func (t *T) Pointer() string { return "pointer:" + t.name } // pointer receiver
 
func main() {
	v := T{"v"}        // v is an addressable variable
	p := &T{"p"}       // p has type *T
 
	fmt.Println(v.Value())   // OK: Value is in method set of T
	fmt.Println(v.Pointer()) // OK: shorthand for (&v).Pointer(); v is addressable
	fmt.Println(p.Value())   // OK: (*p).Value(); Value is in method set of *T (case 2)
	fmt.Println(p.Pointer()) // OK: Pointer is in method set of *T
}

Line-by-line: v.Value() invokes a value-receiver method on a value — the receiver is a copy of v. v.Pointer() calls a pointer-receiver method on a value; because v is a variable and therefore addressable, the compiler silently rewrites this to (&v).Pointer(). p.Value() calls a value-receiver method through a pointer; *T’s method set includes value-receiver methods (case 2), and the receiver gets a copy of *p. p.Pointer() is the direct, no-rewrite case.

Where the shorthand stops working

type Stringer interface{ String() string }
 
func (t *T) String() string { return t.name }
 
func get() T { return T{"temp"} }
 
func main() {
	m := map[string]T{"k": {"x"}}
 
	// get().String()     // COMPILE ERROR: cannot call pointer method String
	//                    // on get() — the result of a call is not addressable.
	// m["k"].String()    // COMPILE ERROR: m["k"] is a map element, not addressable.
 
	var s Stringer
	// s = T{"y"}         // COMPILE ERROR: T does not implement Stringer
	//                    // (method String has pointer receiver).
	s = &T{"y"}           // OK: *T implements Stringer.
	_ = s
}

Each commented line is a distinct, real compile error. get().String() and m["k"].String() fail because the receiver is non-addressable, so the (&x).String() rewrite is illegal. s = T{"y"} fails because String is excluded from T’s method set — this is a type-level check that does not even reach the addressability question. Only &T{"y"} works: *T’s method set contains String.

Mixed receivers and the standard advice

type Buffer struct{ data []byte }
 
func (b *Buffer) Write(p []byte) (int, error) { // pointer receiver
	b.data = append(b.data, p...)
	return len(p), nil
}
func (b Buffer) Len() int { return len(b.data) } // value receiver — inconsistent!

Mixing receiver kinds on one type is legal but discouraged. The method set of Buffer here is {Len}; the method set of *Buffer is {Write, Len}. A consumer who holds a Buffer value can call Len but not Write; a *Buffer holder gets both. The Go wiki on method sets and Effective Go both advise: pick one receiver kind per type and use it for all methods. If any method needs a pointer receiver (to mutate, or because the type is large), give every method a pointer receiver, so the type is consistently used as *T.

Failure Modes and Common Misunderstandings

“It compiled when I called the method directly, so the type implements the interface.” The most common confusion. v.PointerMethod() succeeding on an addressable v does not mean T satisfies an interface containing PointerMethod. The direct call uses the addressable-receiver shorthand; interface assignment uses the method set, which excludes pointer-receiver methods from T. These are two different rules.

Storing a value in an interface, then being surprised mutations vanish. If you box a *T in an interface and call a pointer-receiver method, mutations are visible because everyone shares the pointee. If you (could) box a T value, each method call would see a copy. Pointer-receiver methods on value types are excluded from the method set partly to prevent the silent “my mutation disappeared” bug.

nil map element method calls. m["missing"].PointerMethod() fails to compile, not at runtime — the diagnosis is “map element is not addressable,” which surprises people expecting a nil-pointer panic instead.

Large value receivers copied on every call. A value receiver copies the entire receiver on each call. For a struct with a big array field, this is a silent performance cost; it is also a correctness trap if the method “modifies” the receiver and the caller expects the change to stick. See Value Semantics and Pointer Semantics.

Embedded types changing the method set unexpectedly. Embedding *T versus T produces different promoted method sets — covered in Struct Embedding. Embedding a value gives the outer struct only the value-receiver promoted methods (in its value form); the pointer-to-outer form gets both.

Alternatives and When to Choose Them

The choice is not “which alternative tool” but “which receiver kind,” and the community guidance is clear:

  • Use pointer receivers when the method mutates the receiver, when the struct is large enough that copying is wasteful, or when the type contains a sync.Mutex or other field that must not be copied. Once any method needs a pointer receiver, make all methods pointer receivers for consistency, and treat the type as a *T type.
  • Use value receivers for small, immutable-by-convention types — many basic-type wrappers, small structs of a few scalar fields, and types where copy semantics are desired (e.g. time.Time uses value receivers). Value receivers also let T (not just *T) satisfy interfaces, which can be convenient.
  • Never mix the two on one type without a strong reason; the split method set is a maintenance hazard.

Production Notes

The standard library is a useful reference for the convention. bytes.Buffer uses pointer receivers throughout — you always pass *bytes.Buffer and store it in io.Writer. time.Time uses value receivers throughout — it is a small, comparable, copy-friendly value, so a time.Time value satisfies fmt.Stringer directly.

A recurring real-world bug: a type with a pointer-receiver UnmarshalJSON or Scan method, then a slice []T passed to json.Unmarshal or a database/sql Scan. Because elements of a slice are addressable, ranging with &slice[i] works; but ranging with a copy for _, x := range slice { json.Unmarshal(b, x) } passes a non-pointer and silently does nothing useful, or fails to compile if the API demands the pointer-receiver interface. The fix is to range by index and take &slice[i].

Another: passing a sync.WaitGroup or sync.Mutex by value. These types have pointer-receiver methods and must not be copied; go vet’s copylocks analyzer catches accidental value copies. The pointer-receiver design is deliberate — it nudges callers toward sharing rather than copying.

See Also