Method Sets

Understand method sets in Go, the difference between T and *T method sets, and how they govern interface satisfaction and method calls.

A type’s method set is the group of methods the type makes available. Method sets determine whether a type can satisfy an interface, and they explain why you can sometimes call a pointer-receiver method on a value — and why you sometimes cannot. This distinction sits at the center of Go’s implicit interface implementation and its compile-time safety guarantees.

What a Method Set Is

Every named type in Go has a method set. The method set is exactly the set of method signatures available on that type — nothing more. It is defined purely by the receiver types used in method declarations, not by what feels convenient.

From the Go spec:

  • The method set of an interface type is the set of methods declared in that interface.
  • The method set of a named type T consists of all methods declared with receiver type T.
  • The method set of the pointer type *T consists of all methods declared with receiver *T plus all methods declared with receiver T.

That last rule is the one people miss. *T gets the value-receiver methods for free. T does not get the pointer-receiver methods. This asymmetry is the entire reason method sets exist as a concept.

No implicit conversion in the method set itself:

The method set is what is true on paper. Whether the compiler helps you with a convenience rewrite during a call is a separate mechanism — the method set never changes. This separation prevents confusion about what a type can do at the interface level.

Method Set of T Compared to *T

Given these declarations:

type Counter struct { n int }
func (c Counter) Value() int   { return c.n }
func (c *Counter) Increment()  { c.n++ }
  • The method set of Counter is: Value() int
  • The method set of *Counter is: Value() int and Increment()

That single line is worth memorising. A value of type Counter does not have Increment in its method set. A pointer of type *Counter has both.

The method set governs interface assignment:

When the compiler checks whether a concrete type satisfies an interface, it looks strictly at the method set — not at what calls are syntactically convenient. If an interface requires Increment(), only *Counter can be assigned to it, never Counter.

Why the Compiler Lets You Call Pointer Methods on Addressable Values

Go gives you a shortcut: if you call a pointer-receiver method on a value that is addressable, the compiler silently rewrites the call to take the address of that value first.

c := Counter{}
c.Increment()   // rewritten to (&c).Increment()

This works because c is a local variable — it has a stable memory address. The method call is legal on the pointer, so the compiler does the conversion for you.

The method set of Counter has not changed. The compiler is just helping. If you try to use Counter where the language truly demands the method set — such as in an interface assignment — the compiler cannot help, and the assignment will fail.

type Updater interface {
    Increment()
}
var u Updater
u = Counter{}   // compile error: Counter does not implement Updater (Increment has pointer receiver)
u = &Counter{}  // ok

The variable c could call Increment(), but Counter still failed as an interface value. That is the precise line between syntactic convenience and the method set.

Addressability is not universal:

Map elements, return values of functions, and values stored inside interfaces are not addressable. For those, you cannot use the convenience rewrite — the call is simply illegal.

Addressability Constraints That Surprise Beginners

The following cases are not addressable, so calling a pointer-receiver method on them will not compile:

  • Map elements indexed directly: m["key"] is not addressable.
  • Function return values used directly: f().Method() where f() returns a value type.
  • Explicit unaddressable temporaries: literals like Counter{}.Method() where Method has pointer receiver.

Here is the map case in detail:

lists := map[string]Counter{}
lists["a"].Increment() // compile error: cannot call pointer method on Counter value in map

The expression lists["a"] is not addressable — the map may reallocate and move elements, so there is no stable address. The compiler cannot rewrite it to (&lists["a"]).Increment().

The solution is to store pointers in the map:

lists := map[string]*Counter{}
lists["a"] = &Counter{}
lists["a"].Increment() // works

The same restriction applies when a method is called on a concrete value stored inside an interface variable. An interface value is not addressable, so you cannot call a pointer-receiver method through it even if the underlying concrete type could.

var u Updater = &Counter{}
u.Increment() // works because Updater.Increment is part of the interface
// but:
type Descriptor interface { Value() int }
var d Descriptor = Counter{}
d.Value() // works (Value has value receiver)
// However, if Descriptor had only pointer-receiver methods, you could never store a plain Counter in it.

Interface Satisfaction and Method Sets

An interface requires a set of methods. A concrete type satisfies that interface if its method set contains every method the interface demands. The rules become stark:

  • If every method in the interface has a value receiver, both T and *T satisfy it.
  • If any method in the interface has a pointer receiver, only *T satisfies it.

Consider an interface that mixes both:

type Printer interface {
    Print()
    Reset()
}
type Document struct { text string }
func (d Document) Print() { fmt.Println(d.text) }
func (d *Document) Reset() { d.text = "" }

Document does not satisfy Printer because Reset is missing from its method set. *Document does satisfy it.

This is why Go libraries often define interfaces containing only value-receiver methods when they expect both values and pointers to satisfy the interface. Alternatively, they accept only pointers in constructors that return concrete types.

Compilation will stop you, but only for interfaces:

The compiler will not warn you about calling a pointer method on an unaddressable map value unless that call appears directly. But assigning that same value to an interface with that method will always fail at compile time. That compile-time check is what makes method sets a safety net.

Example Walkthrough Using a Slice Type

The List example from the Go wiki is the canonical demonstration.

type List []int
func (l List) Len() int          { return len(l) }
func (l *List) Append(val int)   { *l = append(*l, val) }

Method sets:

  • List: Len() int
  • *List: Len() int, Append(int)

Both value and pointer variables can call Append if addressable:

var lst List
lst.Append(1)       // ok: &lst is taken, Append called
fmt.Println(lst.Len()) // ok
plst := new(List)
plst.Append(2)       // ok: already a pointer
fmt.Println(plst.Len()) // ok: pointer can call value method

Now define interfaces:

type Appender interface {
    Append(int)
}
type Lener interface {
    Len() int
}

Assignment results:

var app Appender = lst   // compile error: List does not implement Appender
var app Appender = plst  // ok: *List has Append
var l Lener = lst       // ok: List has Len
var l Lener = plst      // ok: *List has Len (inherited from List)

This table shows what works at the interface level, which reflects method sets precisely.

Choosing Value or Pointer Receiver — A Practical Guide

While method sets are a specification concept, they influence everyday design decisions. The rule of thumb that emerges:

  • Use a value receiver when the method does not mutate the receiver and the type is small. This keeps the method set of T as rich as possible, making it easy to satisfy interfaces with plain values.
  • Use a pointer receiver when the method must mutate the receiver, or when the receiver is large and copying it on every call would be wasteful. This will restrict interface satisfaction to *T, but that often aligns with the fact that users already hold a pointer.

In practice, many types end up mixing receivers. When you do, just remember: if any method needs a pointer receiver, T will not satisfy interfaces that require those methods — only *T will.

If your code compiles and values satisfy the interfaces they need:

If you find that a plain value type satisfies all the interfaces your program expects, and all method calls work, your receiver choices are consistent with your design. This is the signal that you’ve correctly navigated method sets.

Summary

Method sets are the honest inventory of what a type can do when it matters most — at interface boundaries. The compiler’s convenience rewrites (address-of insertion and automatic dereferencing) make everyday method calls feel uniform, but they do not expand the method set itself.

  • *T always includes the method set of T; the reverse is never true.
  • A value is addressable in many places, but not inside maps, interface values, or temporary return values.
  • Interface satisfaction is determined strictly by the method set, not by call convenience.

Understanding this distinction gives you a precise mental model for why some interface assignments fail, why maps of values can feel restrictive, and how to design types that work well with Go’s implicit interfaces.