Interfaces in depth
Foreword
A conversation with a colleague inspired a deep dive into the refspec, to gain concrete understanding on some things I had until then only had intuitions about. I ended up writing nearly an article to that colleague in chat (sorry colleague…), so I thought I’d take it over the finish line and actually write the article that my chat message was trying to be.
This is that article.
Impetus
This article is inspired by the following confusion:
type myInterface interface{ hello() }
var m1 myInterface = implementsMyInterface{}
m1.hello() // works!
var m2 *myInterface = &m1
m2.hello() // does not work
The core question is: Why can we use m1
, not m2
?
TLDR: Go veterans will realise that pointer to interface is an anti-pattern. It represents kind of a misunderstanding of what’s going on: the user almost certainly wants a pointer to the struct. Both concrete structs and pointer to structs can implement interfaces. That’s the intuition I mentioned above. But, let’s dive into this a bit and figure out what’s behind this.
What are interfaces, anyway?
From Laws of reflection,
A variable of interface type stores a pair: the concrete value assigned to the variable, and that value’s type descriptor.
So, I’ll simplify this a bit to the hand-wavy description that an interface type points to a concrete type. For example, consider an interface type that is implemented by a struct:
var foo someInterface = someStruct{}
Here, foo
is a variable whose type is someInterface
. Its interface type
“points” (“holds” / “is assigned” / etc) to someStruct
.
Let’s modify that a bit:
var foo someInterface = &someStruct{}
Here, foo
is a variable whose type is someInterface
which points to a
pointer which points to someStruct
.
Ok… still in normal territory. Now let’s go back to where this question came
from and consider if foo
was of type *someInterface
instead:
var foo someInterface = someStruct{}
var bar someInterface = &someStruct{}
var gaz *someInterface = &foo // bad
var urk *someInterface = &bar // also bad
Here, gaz
and urk
are pointers to an interface. That’s almost certainly user
error. Pointers to structs are useful,
- They allow us to avoid copying when passing structs around.
- They allow us to modify and persist struct state.
But, what does a pointer to an interface type give us? Nothing!
- An interface type is already a type of pointer as it is, so there’s no copying when we pass it around.
- An interface type has no state itself. Its implementing type - a struct - can, but not the interface itself. So there’s no modify/persist state benefit.
Interfaces behave different than structs
Ok, so we’ve talked about how interfaces are fundamentally different, and that variables conceptually should (usually) not use interface pointer types; in constrast to structs, where pointers to structs are very common and useful.
Now let’s look at how struct and interface types behave differently with regards to pointer referencing and dereferencing. Let’s do so by collecting together a few facts about interfaces from the refspec, to prepare for our conclusion:
A struct can implement an interface with concrete or pointer method receivers
A struct can implement an interface with either concrete or pointer method receivers. Per ref/spec#Interface_types, there’s no way to specify concrete or pointer method receiver in an interface. (indeed, it’s moot to the interface: the interface defines, well, the interface, not the implementation details)
Concretely, both these structs implement the interface:
type myInterface interface{ hello() }
type concreteMethodReceivers struct{}
func (m concreteMethodReceivers) hello() {}
type pointerMethodReceivers struct{}
func (m *pointerMethodReceivers) hello() {}
Selectors automatically dereference pointers to structs
Selectors automatically dereference pointers to structs ref/spec#Method_values:
As with selectors, a reference to a non-interface method with a value receiver using a pointer will automatically dereference that pointer: pt.Mv is equivalent to (*pt).Mv.
So, for implementing interfaces:
- If you have a struct that implements the interface with concrete method receivers, you can use either concrete struct or pointer to your struct as type for interface (latter will be de-referenced).
- If you have a struct that implements the interface with pointer method receivers, you have to use pointer to your struct as type for interface (concrete struct won’t be automatically turned to pointer).
Concretely:
var f1 foo = concreteMethodReceiverStruct{}
f1.Hello() // works
var f2 foo = &concreteMethodReceiverStruct{}
f2.Hello() // works
var f3 foo = pointerMethodReceiverStruct{}
f3.Hello() // does not work
var f4 foo = &pointerMethodReceiverStruct{}
f4.Hello() // works
Pointers to interfaces do not automatically dereference
Pointers to interfaces do not automatically dereference, like pointers to structs do. (they used to in pre-Go1, fwiw; g/golang-nuts/c/RhIIHM3XC4o)
Concretely:
var thing myInterface = myStruct{}
thing.Whatever() // works
var thing2 *myInterface = &thing
thing2.Whatever() // does not work
Putting it all together
So, let’s talk about the various ways you can declare and use an interface. As mentioned before and shown with play/p/aWQ8C2-SwZ2:
- ✅ can implement interface with concrete type method receiver + concrete type
- ✅ can implement interface with concrete type method receiver + pointer type (auto de-reference)
- ❌ can implement interface with pointer type method receiver + concrete type (no auto reference)
- ✅ can implement interface with pointer type method receiver + pointer type
Now, let’s contrast that with the various ways that you can declare and use a struct. As shown with play/p/IfD0MGTLT_n and spelled out in ref/spec#Method_values:
- ✅ can call concrete type method receiver with concrete type
- ✅ can call pointer type method receiver with concrete type (auto reference)
- ✅ can call concrete type method receiver with pointer type (auto de-reference)
- ✅ can call pointer type method receiver with pointer type
So, the rules for structs and interfaces are different, to prevent interface mis-use.
Afterword
You can read more about how interfaces are represented here:
- Go Data Structures: Interfaces by rsc@
- Go Interfaces by iant@
- Laws of Reflection by r@