Skip to main content

Go Method Receivers: Why They Don't Mix

·702 words·4 mins· loading · loading ·
Go
Natercio Moniz
Author
Natercio Moniz
Table of Contents

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())
}
Play

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)
}
Play

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:

The Method Set of *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:

flowchart LR subgraph *Foo subgraph Foo String end Set end

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:

While it’s safe to obtain a value by dereferencing any pointer to a type the reverse is not.

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.