Introduction#
When I began learning Go, one of my primary resources was A Tour of Go. It’s a fantastic starting point, but I found the Interfaces page somewhat lacking. While the problem presented is clear, it took me a while to fully understand the concepts at play.
In this post, I’ve gathered insights from various sources - Stack Overflow, the Go FAQ, the Go Spec, and more - to clarify that problem. While seasoned Gophers may not find this article groundbreaking, I hope it can be a valuable resource for those just starting out.
Understanding Receivers#
Let’s start by considering the following offending type:
type Foo struct {
i int
}
func (f Foo) String() string {
return fmt.Sprintf("%d", f.i)
}
func (f *Foo) Set(i int) {
f.i = i
}
Any experienced Gopher will likely advise to just use pointers for all receivers in the previous type.
But it’s perfectly fair to argue that the String
method doesn’t really need a pointer receiver.
Also, as you may have come to expect, both methods can be called with either Foo
or *Foo
instances anyway.
Just look at the example below:
func main() {
val := Foo{i: 1}
val.Set(2)
println(val.String())
ptr := &val
ptr.Set(3)
println(ptr.String())
}
So, if we did call Set
with both pointer and value receivers in the previous example why does the following code (like the example in Interfaces) fail to compile:
type Setter interface {
Set(int)
}
func main() {
val := Foo{i: 1}
val.Set(2)
var a Setter
a = val
a.Set(3)
}
You should get an error like this:
cannot use val (variable of type Foo) as Setter value in assignment: Foo does not implement Setter (method Set has pointer receiver)
The reason the first example works is because Go automatically dereferences and references the operant to match whatever the method receiver needs.
The val.Set(2)
call is the same as (&val).Set(2)
and the ptr.String()
call is equivalent to (*ptr).String()
.
The reason the second example fails is more complex.
To fully understand why we get that error we need to first understand what Method Sets are and how they work. While you should most definitely read the formal definition (here) I’ll just cut to the relevant learning:
*Foo
consists of all the methods with *Foo
and Foo
receivers, while the Method Set of Foo
consists of only the methods with Foo
receivers.Here is a simple visualisation of the Foo
and *Foo
Method Sets:
When checking if a type implements an interface it can be more useful to ask: Does a type Method Set satisfy an interface Method Set?
This explains the error in the last example, since Method Set of Foo
does not contain a Set(int)
method.
You might be wondering why doesn’t Go offer the same conveniences as method invocation when it comes to interfaces. Rest assured there’s a good reason for it. You can find it in the FAQ but here’s the TL;DR:
Lets pretend Go did this auto-referencing for us to see the problem in action. Consider the following code:
func SetIt(s Setter, i int) {
s.Set(i)
}
func main() {
val := Foo{i: 1}
SetIt(val, 2)
}
If Go were to accept val
as a valid Setter
then the method Set
call would be setting the i
on the copy of val
(SetIt
argument) which, as the FAQ suggests, is probably not what you were expecting.
Conclusion#
There’s nothing particularly wrong about how Go handles mixed receiver types; Go will automatically reference and dereference receivers for each method call. However, this practice can lead to inconsistent code1 and some confusion when interfaces are involved - and interfaces are almost always involved.
There’s also a potential data race that might arise from using mixed receivers mentioned by Dave Cheney in this blog post.
If you’re looking for guidelines on handling receivers, adhere to these basic rules, and you should be fine. In general, it’s also wise to follow the recommendations found in the Go Code Review Comments.