Type embedding: Golang's fake inheritance

7 min read

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:

I made this

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.

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.