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 abufio.ReadWriter
is invoked… the receiver is thereader
field of theReadWriter
, not theReadWriter
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:
-
We define the relationship between implementations using composition of data structures. That is, a
Bar
supports all the data and methods of aFoo
. -
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.