It's been a very long time since I had any formal education in programming. At that time, when I was in college, we talked a lot about inheritance. From my perspective it seemed synonymous with Object-Oriented Programming. We talked very little about composition, if at all. So it was a struggle for me to transition from inheritance-based programming in C++ and Python, to a composition approach in Go.
This essay tries to help others make the same migration, though I admit I don't know how common it is today. If you're already comfortable with composition-based programming, this story may not be very interesting to you. Also, this story is NOT about trying to convince the reader that composition is better or worse than inheritance.
With all that in mind...
I've been writing Go for a very long time. Go 1.0 was released in 2012, and I think I was using the beta even before that first official release. But at the latest, I was definitely writing copious Go code by early 2013. At that time, I was entirely self-taught, reading the documentation and hacking on experimental projects. The language was so new that community tutorials and other engagement were relatively rare at that time, so there wasn't much to go on besides the official tutorials.
Before writing Go, I had written C++ and Python extensively, since around 2000. Those languages, of course, use an inheritance-based approach to Object Oriented Programming, and by 2012 I had enough hubris to believe I could inheritance the **** out of any problem. At that time, I had never encountered a problem that couldn't be solved by the strategic application of a well-crafted class hierarchy.
Go, of course, takes a composition approach. But in my hubris, I saw Go's "embedded types" feature and thought "Good enough! Let's do some inheritance!"
It was an alluring approach. The embedding strategy worked right up until the point that it didn't, but this was often late in a project after I'd written many hundreds or thousands of lines of code. But then I'd get to a place where I needed more proper inheritance. I'd try to do a workaround but then there was an import cycle. Then I started reaching for manually constructed vtables... This can't be right!
After a year or more of flailing (I may be a slow learner), eventually Go's more composition-oriented model clicked for me. Over the 10+ years after that revelation, I've tried several times to write some short code snippets to illustrate and compare the two styles. But it's so difficult! I start writing the inheritance approach, and I inevitably think, "Why would I ever structure code this way?? The Go/composition approach is Just Better!" And now even when I write Python code, I use a very Go-like style.
But I've tried one more time power through. After two hours of struggling, I've put together this simple, classic inheritance example:
class Shape:
def __init__(self, color):
self.__color = color
# Just a demonstration of a simple base class behavior
def color():
return self.__color
# This method relies on a virtual method on the subclass.
def longest_side(self):
return max(self.sides())
# This method must be defined by the subclass.
def sides(self):
raise NotImplementedError
class Rectangle(Shape)
def __init__(self, color, width, height):
super.__init__(color)
self.__width = width
self.__height = height
# Satisfy the required subclass method
def sides():
return [self.__width, self.__height, self.__width, self.__height]
In this common inheritance pattern, the parent class is intuitively embedded inside the child class. The child class relies on the parent's common, shared behaviors (longest_side()
in this case), then adds on a little extra around that. The parent's vtable allows dispatch of methods to the child's method overrides (sides()) in order to contribute to the parent's common functionality.
In 2012, I would have attempted a similar parent-child inheritance approach with Go, wrapping up the parent "class" inside the child "class".
// This is not a recommended pattern!
type Shape struct {
color string
}
func (s *Shape) Color() string {
return s.color
}
func (s *Shape) LongestSide() float64 {
return slices.Max(s.Sides()) // <-- this doesn't work!
}
type Rectangle struct {
Shape // unnamed field adds Shape's methods onto Rectangle's method set
width float64
height float64
}
func (r *Rectangle) Sides() []float64 {
return []float64{r.width, r.height, r.width, r.height}
}
Unfortunately, not only is this not a recommended pattern, it doesn't even work. Since Go doesn't have vtable-style object inheritance, when we try to execute LongestSide()
, the Shape
object has "forgotten" that it's a Rectangle
. That context has been lost, and all we have is what exists in the Shape
"base class" itself.
There are a variety of workarounds that one can use, such as perhaps passing the Rectangle
itself as an argument to Sides(), so the Shape
can get access to it. But while these approaches technically work, they always felt like an abomination of API design. (In the following examples, I've removed the Color behavior of Shape
for brevity.)
// This pattern is also not recommended!
type ShapeDetails interface {
Sides() []float64
}
type Shape struct {}
// LongestSide takes a reference to the Rectangle (abstracted by an interface)
// such that Shape can access the subclass behaviors.
//
// ... So then you call this with rectangle.LongestSide(rectangle)?? Gross!
func (s *Shape) LongestSide(details ShapeDetails) float64 {
return slices.Max(details.Sides())
}
What ended up working well for me is a pattern that I now use nearly everywhere. Instead of bundling the parent class inside the child class, we turn the inheritance model inside-out. The child class gets bundled inside the parent class. More specifically and in Go terminology:
Define a struct that represents the general behavior, then inject one or more additional structs to define what makes this particular instance special.
type Rectangle struct {
width float64
height float64
}
func (r *Rectangle) Sides() []float64 {
return []float64{r.width, r.height, r.width, r.height}
}
// The interface specification describes the behaviors that Shape requires in order to function.
type ShapeDetails interface {
Sides() []float64
}
type Shape struct {
details ShapeDetails
color string
}
func (s *Shape) Color() string {
return s.color
}
func (s *Shape) LongestSide() float64 {
return slices.Max(s.details.Sides())
}
Often I'll combine this pattern with a factory function or constructor to do the assembly in a more controlled way. With this approach, most users of the Rectangle
struct don't need to know the details of how it works or where it comes from.
func NewRectangle(color string, width float64, height float64) *Shape {
return &Shape{
&Rectangle{width, height},
color,
}
}
Now that I'm accustomed to this pattern, I'm incapable of going back. As I mentioned, even the short inheritance Python example in this essay required hours of effort and mental gymnastics to assemble. Perusing the many thousands of lines of Python code that I've written professionally over the past couple years, I found exactly one case of a meaningful class hierarchy, and that instance didn't involve the base class calling virtual methods on the child class.
For me, the composition approach encourages:
But my intention isn't to convince you that the Go/composition approach is better! Rather, I simply hope that by providing this comparison of the composition approach with the inheritance approach, you can avoid the pitfalls that I boldly walked into face-first. Maybe you can be an effective author of Go code in less than the year it took me! 😬
P.S. I have many other thoughts on Go as well. Next time, I'll share the ways I tend to deviate from the Go community style guidelines, and why.