Subclassing vs Embedding in GoLang

This is a topic that I’ve seen pop up in discussions and articles about Go. The Go programming language does not support inheritance, but instead it supports composition via “embedding.”

It can be a little hard to grasp the difference. This is especially so if you’ve worked in a language like C++, which implements inheritance by (literally) embedding the superclass object within the subclass object.

However, the semantics of Go’s embedding fail to support inheritance in important ways, and there are other Go features and idioms that we use to implement inheritance semantics (or something like them).

In its section on “Embedding,” Effective Go says:

There’s an important way in which embedding differs from subclassing. When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one. In our example, when the Read method of a bufio.ReadWriter is invoked… the receiver is the reader field of the ReadWriter, not the ReadWriter itself.

Let’s demonstrate this, starting with a more contrived but perhaps simpler example:

package main

import "fmt"

type Foo struct {
	x int
}

type Bar struct {
	Foo
	y int
}

func main() {
	b := Bar{Foo{10}, 20}
	fmt.Println("x =", b.x)
	fmt.Println("y =", b.y)
}

I’ve declared a couple of “classes,” Foo, analogous to the superclass, and Bar, analogous to the subclass. Each of these classes has a data member unique to itself, but Bar also embeds Foo. I say “analogous to,” because these two structures do not enjoy a superclass/subclass relationship, as we shall soon see.

In main(), I create an instance of Bar, initialized with x of 10 and y of 20. This code prints the following output:

x = 10
y = 20

This is exactly what you’d expect from a language that supported inheritance. But what the Go compiler is actually doing here is that when I refer to b.x, it’s resolving it to b.Foo.x. This shortcut makes it seem like x is a member of b, but make no mistake: it’s a member of Foo.

Go’s embedding does not support the Liskov Substitution Principle. This is a key concept in object-oriented programming. The idea is that if Bar is a subclass of Foo, then a Bar is able to fill in wherever a Foo is asked for.

Let’s try this in Go (replacing main() in the above):

func PrintX(f Foo) {
	fmt.Println("x =", f.x)
}

func PrintY(b Bar) {
	fmt.Println("y =", b.y)
}

func main() {
	b := Bar{Foo{10}, 20}
	PrintX(b)
	PrintY(b)
}

This gives the error “cannot use b (type Bar) as type Foo in argument to PrintX.”

In order to get the code to work, we need to replace PrintX(b) with PrintX(b.Foo), after which we get the expected:

x = 10
y = 20

But that’s hardly a solution. That demonstrates that a Bar cannot be provided where a Foo is needed.

In particular, what if we wanted to print the name of the class along with the data, in order to get something like:

Foo.x = 10

…if we’re accessing through a Foo object and:

Bar.x = 10

…if we’re accessing through an object of type Bar. We have no mechanism through which to even imagine that… yet.

Enter interfaces. Interfaces in Go provide polymorphic dispatch of methods. In Go, the methods themselves are associated with the object types, but the dispatch table is associated with the interface. We need to define methods on our objects that satisfy declared interfaces, then use the objects through the interfaces that they support.

So for example, let’s start by defining our Foo class:

type Foo struct {
	x int
}

We’ll need a method to access the data. (You’ll see why in a moment.) So let’s define a method to access x:

func (f Foo) X() int {
	return f.x
}

This is a method that gets called on an object of type Foo. We also want to be able to print the name of the class:

func (Foo) Name() string {
	return "Foo"
}

Let’s do something similar for Bar, where Bar embeds Foo:

type Bar struct {
	Foo
	y int
}

func (b Bar) Y() int {
	return b.y
}

func (Bar) Name() string {
	return "Bar"
}

We want to be able to access these polymorphically. So let’s declare interfaces that express our preferred API to these classes:

type Named interface {
	Name() string
}

type NamedX interface {
	Named
	X() int
}

type NamedY interface {
	Named
	Y() int
}

A Named object supports the Name() method that returns the name of the thing being represented. A NamedX object does everything a Named object does, plus it supports an X() method that returns the value of x. Similarly, NamedY support Y().

Go interfaces are duck-typed. That is, if it quacks like a duck and swims like a duck and waddles like a duck, then it’s probably a duck. If it does all the things a duck does, then we treat it as a duck. And if it does all the things that a duck does, then we can use it anywhere we require a duck.

That sounds like the Liskov Substitution Principle. Or a variation thereof.

Any object that supports the interface of a duck can be used wherever a duck is required. Any object that supports the NamedX interface can be used wherever a NamedX is required, no matter what its underlying data structure.

That means we can write:

func PrintX(x NamedX) {
	fmt.Printf("%s.x = %d\n", x.Name(), x.X())
}

func PrintY(y NamedY) {
	fmt.Printf("%s.y = %d\n", y.Name(), y.Y())
}

And if the power of this has not quite hit you yet, consider the following:

func main() {
	f := Foo{10}
	PrintX(f)
	b := Bar{f, 20}
	PrintX(b)
	PrintY(b)
}

This produces the output:

Foo.x = 10
Bar.x = 10
Bar.y = 20

Let’s go into just a little more detail of what’s happening here. When we call PrintX(b), the Bar object supports the Name() method through func (Bar) Name() string. However, it supports the X() method via its embedded Foo object, through func (f Foo) X() int. Each method is called on the Bar or Foo object that is its receiver. All the magic is done by the interface, which knows that the shortcut b.X() actually refers to b.Foo.X().

There are two different problems solved in the code above, by two separate language features:

  1. We define the relationship between implementations using composition of data structures. That is, a Bar supports all the data and methods of a Foo.

  2. We define API semantics using interfaces. In other words, a NamedX supports all the capabilities of a “named X” object, no matter how it is implemented underneath. This is where polymorphism lives.

In most programming languages, these two aspects of objects are combined and entangled. The way we define our classes itself declares the API, and vice-versa.

In GoLang, they are separate and orthogonal. That’s why NamedX and NamedY don’t refer at all to Foo or Bar, and vice-versa.

I suspect that this conceptual disconnect is why people sometimes have trouble grasping these GoLang features, and I hope that now they make a little more sense, and a little more sense how to use them.

Still typing…


Featured image: The GoLang mascot and logo were designed by Renée French, who also designed Glenda, the Plan 9 bunny. The gopher is derived from one she used for a WFMU T-shirt design some years ago. The logo and mascot are covered by the Creative Commons Attribution 3.0 license.

This entry was posted in Uncategorized and tagged , . Bookmark the permalink.

Leave a reply