Type embedding: Golang's fake inheritance
Last week a complaint from a gopher about his coworkers bringing Java conventions to a Golang codebase made a minor splash on Reddit. Their complaints:
- All context.Context are currently being stored as fields in structs.
- All sync.WaitGroups are being stored as fields in structs.
- All channels are being stored as fields in structs.
- All constructor functions return exported interfaces which actually return unexported concrete types. I'm told this is done for encapsulation purposes, otherwise users will not be forced to use the constructor functions. So there is only ever one implementation of an interface, it is implemented by the lower case struct named the same as the exported interface. I really don't like this pattern.
- There are almost no functions in the code. Everything is a method, even if it is on a named empty struct.
- Interfaces, such as repository generally have tons of methods, and components that use the repositories have the same methods, to allow handlers and controllers to mock the components (WHY NOT JUST MOCK THE REPOSITORIES!).
They have a point (and many redditors agreed). But in defense of their coworkers, these are aesthetic concerns, not practical correctness issues.
But in my experience, there's one very major trap that people fall prey to when coming to Golang from an object-oriented language like Java: type embedding.
What's type embedding?
In Golang, it's possible to embed a type inside another type by using a struct. This looks like a normal field declaration but without a name. Here's embedding one struct inside another:
type inner struct {
a int
}
type outer struct {
inner
b int
}
inner
has been embedded into outer
. This gives you a syntactic
shortcut for accessing all of the embedded type's fields. For example,
you can do this:
var x outer
fmt.Printf("inner.a is %d", x.a)
Even though the outer
struct doesn't have a field called a
, the
embedded inner
struct does. It's a shortcut for typing this:
var x outer
fmt.Printf("inner.a is %d", x.inner.a)
But the reason most people use this feature, especially those of us who come from an object-oriented background, is that it allows you to share methods on the embedded type. At first glance, it looks like inheritance (but it's not, as we'll see).
If I give inner
a method, I can call it on instances of outer
. And
outer
now also satisfies interfaces that need that method. Here's a
full example demonstrating this capability:
type printer interface {
print()
}
type inner struct {
a int
}
func (i inner) print() {
fmt.Printf("a is %d", i.a)
}
type outer struct {
inner
b int
}
func main() {
var x printer
x = outer{inner{1}, 2}
x.print()
}
That all works and does exactly what you expect. Pretty neat, right? You're probably thinking you could save a lot of boilerplate code by using this feature, and that's true. But there's a huge caveat: Type embedding isn't inheritance, and pretending it is will lead to bugs.
Let's examine a few common use cases where the analogy between type embedding and inheritance breaks down and discuss why that is.
this
is special
Let's look at a classic object-oriented tutorial about talking animals. Here it is in Java:
public class Animals {
public static void main(String[] args) {
Animals example = new Animals();
example.Run();
}
public void Run() {
Animal a = new Tiger();
a.Greet();
}
interface Animal {
public void Speak();
public void Greet();
}
class Cat implements Animal {
public void Speak() {
System.out.println("meow");
}
public void Greet() {
this.Speak();
System.out.println("I'm a kind of cat!");
}
}
class Tiger extends Cat {
public void Speak() {
System.out.println("roar");
}
}
}
So in this example, we have an Animal
interface that declares two
methods. Then we have two implementors Cat
and Tiger
. Tiger
extends Cat
and overrides one of the interface methods. When you run
it, it produces the output you would expect:
roar
I'm a kind of cat!
Now let's duplicate the same thing in Golang, pretending that type embedding is the same as inheritance.
type Animal interface {
Speak()
Greet()
}
type Cat struct {}
func (c Cat) Speak() {
fmt.Printf("meow\n")
}
func (c Cat) Greet() {
c.Speak()
fmt.Printf("I'm a kind of cat!\n")
}
type Tiger struct {
Cat
}
func (t Tiger) Speak() {
fmt.Printf("roar\n")
}
func main() {
var x Animal
x = Tiger{}
x.Greet()
}
Java refugees should take a moment to appreciate how much useless boilerplate and semicolons have been eliminated. But despite being more compact, this code has a big problem: it doesn't work. When you run it, you get this output:
meow
I'm a kind of cat!
Why is our Tiger meowing instead of roaring? It's because the this
keyword in Java is special, and Golang doesn't have it. Let's put the
two Greet
implementations side by side to examine.
public void Greet() { // method in Cat class
this.Speak();
System.out.println("I'm a kind of cat!");
}
func (c Cat) Greet() {
c.Speak()
fmt.Printf("I'm a kind of cat!\n")
}
In Java, this
is a special implicit pointer that always has the
runtime type of the class the method was originally invoked on. So
Tiger.Greet()
dispatches to Cat.Greet()
, but the latter has a
this
pointer of type Tiger
, and so this.Speak()
dispatches to
Tiger.Speak()
.
In Golang, none of this happens. The Cat.Greet()
method doesn't have
a this
pointer, it has a Cat
receiver. When you call
Tiger.Greet()
, it's simply shorthand for Tiger.Cat.Greet()
. The
static type of the receiver in Cat.Greet()
is the same as its
runtime type, and so it dispatches to Cat.Speak()
, not
Tiger.Speak()
.
Embedded dispatch in Golang, illustrated:
Coming from a language like Java this feels like a violation, but
Golang is very explicit about this: the receiver param declares its
static type, and the go vet
tool cautions you against naming
receiver params this
or self
, because that's not what they are.
This same basic problem crops up whenever we attempt to apply our OOP intuitions to Golang using embedding.
Method chaining: an embedded foot-gun
Another common OOP pattern is the practice of method chaining, where
each method returns the this
pointer to itself so that you can call
additional methods on the result without starting a new statement. In
Java it looks something like this:
int result = new HttpsServer().WithTimeout(30).WithTLS(true).Start().Await();
Each of those methods returns this
, which is the magic that lets us
call method after method on the same object in the same statement. It
can be a big space saver and make the code much more readable, when
used effectively.
But in Golang, this same pattern can become a footgun if you mix it
with type embedding. Consider the following toy hierarchy of Server
types, one that supports TLS and one that doesn't:
type Server interface {
WithTimeout(int) Server
WithTLS(bool) Server
Start() Server
Await() int
}
type HttpServer struct {
...
timeout int
}
func (h HttpServer) WithTLS(b bool) Server {
// no TLS support, ignore
return h
}
func (h HttpServer) WithTimeout(i int) Server {
h.timeout = i
return h
}
func (h HttpServer) Start() Server {
...
return h
}
func (h HttpServer) Await() int {
...
}
type HttpsServer struct {
HttpServer
tlsEnabled bool
}
func (h HttpsServer) WithTLS(b bool) Server {
h.tlsEnabled = b
return h
}
func main() {
HttpsServer{}.WithTimeout(10).WithTLS(true).Start().Await()
}
The code above compiles fine and will run, but it has a fatal bug:
WithTimeout
is called on HttpServer
, not HttpsServer
, and
returns the same. Not only is TLS not enabled on your server, but
you're dealing with a completely different struct than it appears from
the main
method.
This is the kind of bug that will have you poring through your debugger for hours trying to figure out what's going on, since the code looks correct at the call site but is tragically flawed. Ask me how I know.
Interface embedding: fine in interfaces, dangerous in structs
The above warnings relate to concrete types, things that can have receiver params. When it comes to interface types themselves, you can embed one interface in another to express interface extension. These Java and Golang snippets are semantically equivalent:
public interface Animal {
public void Eat();
}
public interface Mammal extends Animal {
public void Lactate();
}
type Animal interface {
Eat()
}
type Mammal interface {
Animal
Lactate()
}
This is fine and desirable and doesn't have any of the issues or dangers called out above. But watch out: embedding an interface in a struct is allowed but is almost never what you want. The following compiles but produces a nil pointer panic at runtime.
type Dog struct {
Mammal
}
func main() {
d := Dog{}
d.Eat()
}
You can fix the nil panic by creating a NewDog()
constructor method
that assigns a concrete type to the embedded Mammal
interface
pointer, or by implementing the missing method like so:
func (d Dog) Eat() {
println("Dog eats")
}
This last is usually what people mean when they embed an interface
within a struct: they intend to express that Dog
implements Mammal
and has all the method declared on Mammal
. This is a very dangerous
technique: if you ever add methods to the embedded interface, your
code will still compile but begin throwing nil panics on the new
methods. If you want an equivalent to Java's implements
keyword, use
a static interface assertion instead.
var _ Mammal = Dog{}
It's dangerous but you're going to do it anyway
In spite of the problems with type embedding, the promise of writing less code, of not repeating yourself, is just too seductive to resist. You'll probably end up doing it. And indeed, we use type embedding in Dolt, our version-controlled SQL database we build over here at DoltHub. In spite of its flaws, it's too useful to avoid entirely. We know about these bugs because we've lived them.
In particular, our SQL query planner executes a series of functional transformations that rely on a query plan node being able to return a modified copy of itself, very similar to the method-chaining example in function.
// WithChildren implements the Node interface.
func (d *Distinct) WithChildren(children ...sql.Node) (sql.Node, error) {
if len(children) != 1 {
return nil, sql.ErrInvalidChildrenNumber.New(d, len(children), 1)
}
return NewDistinct(children[0]), nil
}
A node embedding another type and inadvertently getting dispatched to the embedded type's method has been the source of many, many bugs. Having excellent test coverage remains the best way to prevent these and most other bugs, but it's also very helpful to be aware of Golang's limitations, and to write and review code defensively with them in mind.
Conclusion
This blog is part of an ongoing series of technical articles about the Golang language and ecosystem, discussing real issues we've encountered while building the world's first version-controlled SQL database, Dolt.
Like the article? Have questions about Golang, or about Dolt? Come talk to our engineering team on Discord. We're always happy to meet people interested in these problems.