11.3 Method Sets: Which Methods Are in Scope for a Type
Right, let’s talk about method sets. This is where the theoretical “methods are just functions with receivers” meets the practical, rubber-meets-the-road reality of the Go compiler deciding whether you can even call that method. It’s the rulebook, and if you don’t know the rules, you’ll be left shouting at the referee.
The core concept is deceptively simple: a method set is the list of methods attached to a given type. But the scope of that list—which methods are available for you to call in a given context—depends on whether you’re dealing with the type itself (T) or a pointer to the type (*T). And this is where most of the confusion, and frankly, the compiler errors, come from.
The Cardinal Rule of Method Sets
Engrave this on your monitor bezel because it explains 95% of the behavior you’ll see:
- The method set of a value type
Tincludes all methods declared with a value receiver(T). Simple. - The method set of a pointer type
*Tincludes all methods declared with both value(T)and pointer(*T)receivers.
Think of it this way: anything that can read the value can use a value receiver method. But only something that can be pointed to (and therefore modified) can use a pointer receiver method. This leads to the most common “gotcha”.
Why This Asymmetry Exists?
It’s not a design flaw; it’s a deliberate safety feature. Let’s say you have a value receiver method MyValueMethod(). If you have a value myValue, you can call it. If you have a pointer &myValue, the compiler is smart enough to dereference the pointer for you and call the method on the underlying value. It’s safe. No mutation can occur.
Now, consider a pointer receiver method MyPointerMethod(). This method can potentially modify the value it points to. If the language allowed you to call this method on a plain value myValue, what would happen? Go would have to take the address of myValue behind the scenes to pass to the method. But then you’d be modifying a temporary copy, not the original value! This is almost never what you, the programmer, actually intend. It’s a recipe for subtle, maddening bugs. So Go simply forbids it. If you want to call a method that might modify the receiver, you must have its address to begin with.
type Coffee struct {
strength int
}
// Value receiver method (can read, but not modify the struct)
func (c Coffee) Drink() string {
return "mmm, coffee"
}
// Pointer receiver method (can modify the struct)
func (c *Coffee) Refill() {
c.strength = 10
}
func main() {
// Scenario 1: Working with a value
val := Coffee{}
val.Drink() // OK: value can call value method
// val.Refill() // COMPILER ERROR: cannot call pointer method on a value
// Scenario 2: Working with a pointer
ptr := &Coffee{}
ptr.Drink() // OK: pointer can call value method (Go does (*ptr).Drink() for you)
ptr.Refill() // OK: pointer can call pointer method
// Scenario 3: The "behind-the-scenes" addressability
val2 := Coffee{}
val2.Refill() // Still an error. It's not addressable in this context.
(&val2).Refill() // This is perfectly legal and works. You're taking the address explicitly.
}
The Interface Satisfaction Conundrum
This rule becomes absolutely critical when a type is expected to satisfy an interface. A type T implements an interface only if it possesses all the interface’s methods. But remember the method set rule: T only has value receiver methods, while *T has both.
type Brewer interface {
Brew() string
}
type EspressoMachine struct {
beans int
}
// This method has a pointer receiver.
func (e *EspressoMachine) Brew() string {
if e.beans > 0 {
e.beans--
return "Brewing a shot"
}
return "Out of beans!"
}
In this case, the *EspressoMachine type has the Brew() method, so *EspressoMachine satisfies the Brewer interface. The EspressoMachine type (the value) does not have the Brew() method in its method set, so it does not satisfy the interface.
func makeCoffee(b Brewer) {
fmt.Println(b.Brew())
}
func main() {
machine := EspressoMachine{beans: 10}
// makeCoffee(machine) // COMPILER ERROR: EspressoMachine does not implement Brewer
makeCoffee(&machine) // This is correct and works.
}
This trips up everyone. The best practice is to be consistent. If any method on a type needs a pointer receiver (usually because it mutates the state), define all your methods on the type with pointer receivers. This way, both the value and the pointer have a complete method set for the purpose of interface satisfaction, and you avoid this confusing mismatch. It’s a stylistic choice, but one that prevents a whole class of errors.