AlgoMaster Logo

Method Sets

High Priority11 min readUpdated June 6, 2026

A type's method set is the list of methods you can call on a value of that type. Most of the time this list is intuitive: you defined three methods, so the type has three methods. The interesting cases show up at the boundary between value receivers and pointer receivers, because the method set of T and the method set of *T are not the same. This lesson covers what a method set is, how it differs between T and *T, when Go takes the address of a value to call a pointer-receiver method, and the cases where that shortcut breaks down.

What a Method Set Is

A method set is a property of a type. Given a type T, its method set is the set of methods you can call on a value of that type using the dot syntax value.Method(). The Go specification defines two method sets for every concrete type: one for T and one for *T (the pointer to T).

Product has two methods declared on it: Label with a value receiver and ApplyDiscount with a pointer receiver. Both calls in main work, but they don't both work for the same reason, and that's where method sets come in.

Here are the two rules from the Go specification, with no ambiguity:

  • The method set of T contains every method declared with a receiver of type T.
  • The method set of *T contains every method declared with a receiver of type T or *T.

Read that twice. The method set of the pointer type is the bigger one. A value type sees only its value-receiver methods. A pointer type sees both kinds.

Applying this to Product:

TypeMethods in the set
ProductLabel
*ProductLabel, ApplyDiscount

Label shows up in both because it was declared with a value receiver, and the rule for *T includes both kinds. ApplyDiscount shows up only in the *T set because it was declared with a pointer receiver.

Why the Two Sets Differ

The asymmetry exists because calling a pointer-receiver method needs an address to write back into, and that's not always available. A value-receiver method is happy with a copy, so anything can supply it: a pointer, a stored value, a temporary, a constant. A pointer-receiver method needs the original storage location, because changes have to land somewhere durable.

Consider what would happen if *T's methods were callable on a temporary value. The method would modify the temporary, the temporary would vanish at the end of the expression, and the changes would be lost. Worse, the code would look fine at the call site. Go's method-set rules prevent that whole category of silent bug by only including pointer-receiver methods in the *T set.

The diagram shows the directional rule. Value-receiver methods feed into both sets. Pointer-receiver methods feed only into the *T set. The arrows don't go backwards, which is the whole point: from the type alone, the compiler can determine which methods are callable.

The Addressability Shortcut

If the method-set rules were applied strictly at every call site, code would be full of explicit & operators. Calling mouse.ApplyDiscount(10) would be a compile error, because mouse is a Product value, and ApplyDiscount lives in the method set of *Product, not Product. You'd have to write (&mouse).ApplyDiscount(10).

Go saves you from that with one rule: if m is in the method set of *T and x is an addressable value of type T, then x.m() is shorthand for (&x).m(). The compiler takes the address for you, runs the method, and the visible behavior is exactly what you wanted.

item is a CartItem value. AddOne has a pointer receiver, so it lives in the method set of *CartItem, not CartItem. The call item.AddOne() looks like it shouldn't work, but the compiler sees that item is addressable (it's a named local variable, so it has a stable storage location), and it rewrites the call as (&item).AddOne(). The receiver inside the method is *CartItem, the increment lands in the real item, and the change persists.

The same trick goes the other way too, but it's not strictly needed. If m is in the method set of T and you have a *T, then p.m() is shorthand for (*p).m(). The compiler dereferences for you. This direction is less interesting because the method set of *T already contains every value-receiver method, so p.m() is valid by the basic rule anyway.

When the Shortcut Doesn't Apply

The address-taking shortcut only fires when the receiver expression is addressable. Not every value in Go has a stable address. Go's specification lists the addressable cases precisely, and the ones you'll trip over in everyday code are:

ExpressionAddressable?
Named local or package variable (item)Yes
Field of an addressable struct (order.Item)Yes
Element of a slice (items[3])Yes
Pointer dereference (*p)Yes
Element of a map (prices["mouse"])No
Return value from a function (getItem())No
Result of a type conversion (Product(other))No
Composite literal value (CartItem{...})No

A value that isn't addressable doesn't get the free &. Trying to call a pointer-receiver method on one fails at compile time.

The compiler reports something like:

Map elements are not addressable in Go, by design. The map's internal storage can be reorganized when it grows, so any address the compiler handed out could become stale. Rather than let you take an address that might dangle, Go disallows it entirely. That makes prices["notebook"] ineligible for the shortcut, and AddOne (which lives in *CartItem's method set) can't be called on it.

The fix is to copy the value out, mutate it, and put it back:

item is a named local variable, so it's addressable, and the shortcut applies. Storing back into the map replaces the old value with the mutated copy. It's three lines instead of one, but the compiler is honest about what it can and can't do.

A different fix is to make the map hold pointers from the start: map[string]*CartItem. The map element is then already a pointer, no addressability needed, and the call prices["notebook"].AddOne() works directly. Which fix is right depends on whether you want shared mutable state across all lookups (use pointers) or distinct copies per cart (use values and copy back).

Method Sets and Interface Satisfaction

Here's where method sets stop being a curiosity and start mattering for real code. A type satisfies an interface when its method set contains every method the interface requires. A small preview shows why method sets are worth understanding now.

Suppose an interface called Discountable requires one method, ApplyDiscount(percent float64). Does Product satisfy it? Does *Product satisfy it?

ApplyDiscount was declared with a pointer receiver, so it's in the method set of *Product but not Product. The interface requires ApplyDiscount. Therefore *Product satisfies Discountable, and Product does not.

The commented-out line var d Discountable = mouse fails to compile with a message like Product does not implement Discountable (method ApplyDiscount has pointer receiver). The compiler is being literal: the interface's method requirement isn't in Product's method set, so the assignment fails. Taking the address with &mouse switches to the *Product method set, which does contain ApplyDiscount, and the assignment succeeds.

The earlier addressability shortcut does not apply when assigning to an interface. Method-call expressions can implicitly take addresses, but interface assignment uses the strict method-set rule, no shortcut. This is why beginners trip over the same problem in two different shapes: a direct method call on an addressable value seems to work, but the same value can't be assigned to the interface even though the call appeared to satisfy it.

o.AddTax() works because o is addressable and the compiler rewrites the call. var t Taxable = o fails because interface assignment checks the literal method set of Order, which doesn't include AddTax. The fix is var t Taxable = &o.

A Common Gotcha: The Slice of Values Loop

One last pattern. When you range over a slice of values and try to call a pointer-receiver method on the loop variable, the loop variable is addressable, so the call compiles. The trap is that the loop variable is a copy of each element, not a reference into the slice. Modifications land in the copy and disappear at the end of each iteration.

Each call works because item is a named local variable and therefore addressable. The method runs, the copy's Quantity goes up by one, the next iteration overwrites item with the next element, and the increment is lost. The slice never changes.

The fix is to index into the slice directly. Slice elements are addressable, so &cart[i] gives a stable address, and the method modifies the real element.

cart[i] is an addressable slice element, the shortcut takes its address, the method runs against the real element, and the change persists. When you find yourself reaching for a pointer-receiver method inside a range loop, prefer for i := range slice and slice[i].Method() over for _, v := range slice and v.Method().

Quiz

Method Sets Quiz

10 quizzes