As a Go programmer, I’ve encountered numerous scenarios where understanding method sets has been crucial. Today, I want to share insights into this fundamental concept, which often confuses newcomers and occasionally trips up experienced developers.
What are Method Sets?
In Go, a method set is the collection of methods that can be called on a given type. It’s a simple concept with profound implications for how we design and use structs and interfaces.
The Rules of Method Sets
Let’s break down the rules governing method sets for both regular and embedded structs:
Regular Structs
For a pointer type *T
:
- Includes all methods with pointer receivers (
*T
) - Includes all methods with value receivers (
T
)
For a non-pointer type T
:
- Includes only methods with value receivers (
T
) - Does not include methods with pointer receivers (
*T
)
Embedded Structs
The rules become more interesting with embedded structs:
Non-pointer outer struct with non-pointer embedded struct:
- Only includes methods with value receivers from both outer and embedded structs
The following cases all include all methods (both value and pointer receivers) from the embedded struct:
- Non-pointer outer struct with pointer embedded struct
- Pointer outer struct with non-pointer embedded struct
- Pointer outer struct with pointer embedded struct
Let’s illustrate these rules with a concrete example:
type Engine struct{}
func (e Engine) Start() {
fmt.Println("Engine started")
}
func (e *Engine) Stop() {
fmt.Println("Engine stopped")
}
type Car struct {
Engine
}
type Truck struct {
*Engine
}
func main() {
car := Car{}
truck := Truck{&Engine{}}
car.Start() // Works: value receiver method
// car.Stop() // Compile error: pointer receiver method not in method set of Car
truck.Start() // Works: all methods from *Engine are in the method set
truck.Stop() // Works: all methods from *Engine are in the method set
}
In this example:
Car
embedsEngine
as a valueTruck
embedsEngine
as a pointer
The Car
type only has access to the Start
method because it’s a value receiver method. The Stop
method, which has a pointer receiver, is not in its method set.
The Truck
type, on the other hand, has access to both Start
and Stop
methods because it embeds Engine
as a pointer.
Interface Access Example
To further illustrate how method sets affect interface implementation and usage, let’s consider an example with interfaces:
type Starter interface {
Start()
}
type Stopper interface {
Stop()
}
type Vehicle interface {
Start()
Stop()
}
func main() {
car := Car{}
truck := Truck{&Engine{}}
// Interface satisfaction
var s1 Starter = car // Works: Car has Start() in its method set
var s2 Starter = truck // Works: Truck has Start() in its method set
// var st1 Stopper = car // Compile error: Car doesn't have Stop() in its method set
var st2 Stopper = truck // Works: Truck has Stop() in its method set
// var v1 Vehicle = car // Compile error: Car doesn't implement Vehicle (missing Stop())
var v2 Vehicle = truck // Works: Truck implements both Start() and Stop()
s1.Start()
s2.Start()
st2.Stop()
v2.Start()
v2.Stop()
}
This example demonstrates several important points:
Interface Satisfaction: A type satisfies an interface if its method set includes all the methods required by the interface.
- Both
Car
andTruck
satisfy theStarter
interface because they both have theStart()
method in their method sets. - Only
Truck
satisfies theStopper
interface becauseCar
doesn’t have theStop()
method in its method set.
Complete Interface Implementation:
Truck
implements theVehicle
interface because its method set includes bothStart()
andStop()
.Car
doesn’t implementVehicle
because it’s missing theStop()
method in its method set.
Pointer vs Value Receivers:
- The
Car
type (which embedsEngine
as a value) only has value receiver methods in its method set. - The
Truck
type (which embeds*Engine
as a pointer) has both value and pointer receiver methods in its method set.
This example underscores the importance of understanding method sets when working with interfaces in Go. It shows how the choice between value and pointer receivers can affect a type’s ability to satisfy interfaces, which is crucial for designing flexible and reusable code.
A Common Pitfall: Pointer Receivers and Interface Satisfaction
Let’s examine a common pitfall that many Go developers encounter when dealing with method sets and interface satisfaction:
type Stringer interface {
String() string
}
type MyType struct {
value string
}
func (m *MyType) String() string { return m.value }
func main() {
m := MyType{value: "something"}
var s Stringer
s = m // Compile error: cannot use m (type MyType) as type Stringer in assignment:
// MyType does not implement Stringer (String method has pointer receiver)
fmt.Println(s)
}
This example illustrates a subtle but important aspect of method sets in Go:
- We define a
Stringer
interface with aString()
method. - We create a
MyType
struct and implement theString()
method with a pointer receiver(m *MyType)
. - In the
main
function, we try to assign a value ofMyType
to a variable of typeStringer
.
At first glance, it might seem that MyType
implements the Stringer
interface. However, this code will not compile. The error message tells us why: “cannot use m (variable of type MyType) as Stringer value in assignment: MyType does not implement Stringer (method String has pointer receiver)”.
The reason for this error lies in the rules of method sets we discussed earlier:
- The method
String()
is defined with a pointer receiver(m *MyType)
. - According to the rules, methods with pointer receivers are only in the method set of the pointer type
*MyType
, not the value typeMyType
. - When we try to assign
m
(which is of typeMyType
, not*MyType
) to the interface variables
, Go checks ifMyType
implementsStringer
. - Since
String()
is not in the method set ofMyType
(only in*MyType
), the compiler determines thatMyType
does not implementStringer
.
Fixing the Code
To make this code work, we have two options:
- Use a pointer to
MyType
:
func main() {
m := &MyType{value: "something"}
var s Stringer
s = m // This works!
fmt.Println(s)
}
- Change the receiver of the
String()
method to a value receiver:
func (m MyType) String() string { return m.value }
func main() {
m := MyType{value: "something"}
var s Stringer
s = m // This now works!
fmt.Println(s)
}
Key Takeaway
This example highlights a crucial point about method sets and interface satisfaction in Go:
- If you have methods with pointer receivers, only pointer values of that type will satisfy interfaces that include those methods.
- Value types only include value receiver methods in their method set.
Understanding this distinction is vital for correctly implementing and using interfaces in Go. It’s a common source of confusion and errors, especially for developers new to the language.
Conclusion
Method sets in Go might seem complex at first, but they follow a logical pattern that becomes second nature with practice. By understanding these rules, you’ll write more idiomatic Go code and avoid common pitfalls related to method availability and interface satisfaction.
Remember, when in doubt about a type’s method set, you can always refer to the Go specification or use tools like go vet
to catch potential issues early in your development process.