Lost Among Notes

Older >> Comment Threads and Computer Science Fundamentals

Go is a great Language for Object Oriented Programming

In 2017, in Dublin, I gave a presentation on Go as a great language to learn object oriented programming with. I have the slide deck on this blog. My friend John told me recently that he was intrigued by the premise, and that it was worth developing into a post. So here goes.

Premise: Go is not usually thought of as an object oriented language, due to its lack of inheritance. My position on this is that inheritance is not really a core benefit of OO, and that by ignoring it, Go captures what is best of the original message passing concept. 1


I would like to take a step back and recapitulate the rise of Object Orientation, to justify the claim that Go stays close to the original vision.

Before Objects

Let’s imagine an average imperative programming language before the advent of object-oriented languages.

Likely it contains some core types: integers, floats, characters; and also composite data types: arrays and structures.
It also contains functions, and may distinguish functions with no side-effects from procedures which may have side-effects. Of course, the language has the usual control flow constructs, conditionals, and assignment statements.

We’re going to look at two case studies:

Case Study 1: a video game

Let’s imagine Alice, Bob and Chuck are writing a video game together. Alice is writing the main animation loop, and Bob and Chuck are each writing a different foe (this will be a space combat game.)

The code might begin looking like so:

init_bob_foe{...}
init_chuck_foe{...}
init_extra_state_chuck{...}

forever {
    clear_canvas()
    move_uniformly(&bob_foe)
    bob_display_on_canvas(&bob_foe, &canvas)
    move_relativistically(&chuck_foe, &extra_state)
    blah_blah(&extra_state)
    chuck_display(&chuck_foe, &canvas)

With this design, Alice needs to learn how to initialize, update, and display the foes that Bob and Chuck are writing. If they make changes to how those functions need to be invoked, they need to tell Alice about them. The code for each kind of foe is strewn all over the place. It is clear enough now with two kinds of foe, but with a bigger team writing 10 different foes, this would get messy and brittle.

Case study 2: a Database connection library

Don has written a database connection library. Emma is writing a series of database queries and wants to use Don’s library function query to execute them on her servers.

Emma’s code might look like this:

dbConn {
    hostname: foo.bar,
    username: admin,
    password: 12345,
    ConnectionPool: ...
}

main () {
    ...
    query(“my query”, &dbConn)
    ...
}

Emma learns that she can get substantial speedups through special manipulations of the ConnectionPool in the dbConn data structure, and she writes her own function: emma_query.

dbConn {
    hostname: foo.bar,
    username: admin,
    password: 12345,
    ConnectionPool: ...
}

main () {
    ...
    emma_query(“my query”, &dbConn.ConnectionPool)
    ...
}

But Don finds a much better way to do connection pooling, and releases a new version of his library. Which breaks Emma’s code.

dbConn2 {
    hostname: foo.bar,
    username: admin,
    password: 12345,
    NewConnectionPool: ...
}

main () {
    ...
    emma_query(“my query", &dbConn2.AAAARGHH)
    ...
}

Because Don’s DB connection library exposes internal implementation details, it becomes possible for client code to become too tightly coupled. Over time, this makes it difficult for Don to make internal changes, for fear that he might break client dependencies.

The genesis of Object Oriented Programming

I’m going to consider Smalltalk as the origin of Object Oriented design in this post, for pragmatic reasons: I happen to have experience writing Smalltalk, but none with the precursors of OO.

It is well accepted that even before Alan Kay and his team came along, the seeds of class-based programming had been sown in the 1960’s with Dahl & Nygaard’s Simula, and Ivan Sutherland’s Sketchpad.

Alan Kay had a background in biology, and he started to think of self-contained units of computation through a biological metaphor: biological systems manage amazing factors of scaling, and their basic unit is the cell, with internal organelles protected by a membrane, and special channels in the membrane to exchange nutrients and ions with the outside. Lots of cells interact with each other, and the big idea Kay extracted was Message Passing.

a yeast cell

a yeast cell cell image from wikimedia commons

Let’s re-imagine our case studies using this cell-and-messages metaphor.

Database connection library

Don’s library would not be exposing the implementation details of connection pooling. Emma would be provided with the set of messages a DBConn object could process.

the DB connector as a cell

the DB connector as a cell

For example, Emma might write:

myDBConn.query(“select * from foo”)

Video game

Let’s apply the cell metaphor to Bob and Chuck’s foes:

Bob and Chuck’s ships as cells

Bob and Chuck’s ships as cells

Note that the internal state is now hidden inside the cell membrane. In particular, the code that had been handled as init_extra_state_chuck{...} is not exposed.

Interestingly, with the internals hidden, Bob and Chuck’s Foes look very similar. What if Bob and Chuck agreed to rename the messages?

Bob and Chuck’s foes, unified

Bob and Chuck’s foes, unified

This idea is so important it gets a name: Polymorphism. With polymorphism, Alice could treat Bob and Chuck’s Foes interchangeably. Her animation loop could become much simpler:

foes = [bob_foe, chuck_foe]
forever {
    clear_canvas()
    foreach foe in foes {
        foe.update(dt)
        foe.display_on(canvas)
    }
}

With this structure in place, adding 10 different kind of foes to the game seems like a trivial task.

A couple of aspects are worth pointing out:

  • Alice could now specify the messages that are required for a new foe to work for the game. This would enable a much cleaner division of labor, so building new foes could be done in parallel.
  • In the other direction, by establishing the message protocol, Bob and Chuck could reuse or resell their code for other applications consuming the same messages.

Let’s re-cap: by thinking of our case studies using the cell metaphor,

  • Keeping implementation details hidden from the outside makes the design more robust
  • Emphasis shifts to messages between computational units (objects)
  • Polymorphism comes out of that naturally

This is all simple. You could think of moving in this direction as an organizing principle, even using a language like C without specific support for the concepts.

Inheritance is extra bagage

Inheritance is a concept that can be brought to the mix in addition to messages and polymorphism. Notice I wrote “can”. Since the early days of Object Oriented Programming, inheritance was considered a part of the package. Perhaps even the fundamental part of the package:
polymorphism is often conceptualized through the lens of class inheritance. Seen from that vantage point, this is how Bob and Chuck’s Foes look.

polymorphism through inheritance

polymorphism through inheritance

Inheritance got a lot of attention and hope as the tool to accomplish code re-use. But even early on, it was understood that inheritance was not without drawbacks. Note the concepts of abstract classes and pure virtual functions in C++/Java to get polymorphism without implementation inheritance.

The famous book Design Patterns 2, introduced the dictum “Prefer composition to inheritance”.

For a long time, the mainstream object-oriented practice has relied on the up-front creation of class hierarchies to structure code3. Together, of course, with the need to decide on private vs. protected vs. public, abstract or not, static vs instance methods, to name just a few “features”.

C++ made it possible to inherit from two classes, and this brought on even more complexity. Java, in its desire to be simpler than C++, introduced the idea of interfaces, so a class could “inherit” from only one superclass, but “implement” several different interfaces.
This is complex in itself, but Java interfaces also paved the way for Go interfaces.

Go for Object Oriented Programming

In Go, there is no inheritance at all. Go has taken the Java concept of the interface and developed it further into a first-class citizen of the language. In doing so, it has made it into a much more faithful representation of the original message-passing idea, and a more powerful design tool.

In Go, an interface is simply a collection of method signatures. Since in Go any type can have methods 4, no special language construct is needed for classes or instances.

Let’s go back to our video game case study. Both Chuck and Bob had named their two messages “update” and “display_on”.

In Go, Chuck might write something like:

type ChuckFoe struct {
    
}

func (cf *ChuckFoe) Update(dt time.Duration) {
    ...
}

func (cf ChuckFoe) DisplayOn(c Canvas) {
    ...
}

Let’s define the Foe interface. The interface is useful mainly for the code that requires polymorphism. Alice might write something like:

type Foe interface {
    Update(dt time.Duration)
    DisplayOn(c Canvas)
}

func animationLoop() {
    
    foes := []Foe{bobFoe, chuckFoe}
    
    for {
        canvas.Clear(&theCanvas)
        for _, foe := range foes {
            foe.Update(dt)
            foe.DisplayOn(&theCanvas)
        }
    }
}

The Foe interface was not even declared in Chuck’s code. In Go, interfaces are satisfied implicitly, without the need for any sort of implements keyword in the language.

That Chuck and Bob don’t need to declare their code implements the Foe interface is helpful. They can write their code without coupling it to Alice’s definitions. And, their code becomes usable by client code they had never even thought of.

For instance, Emma might want to build a visual catalog of interesting foes in space combat games. Since she just wants to display them and has no use for their Update messages, she might define the following:

type Foe interface {
    DisplayOn(c Canvas)
}

Chuck’s code automatically satisfies this interface. (As will Bob’s.)

Implicit satisfaction of interfaces is a bigger deal than it may seem. It makes possible a nimble approach to design. And, note that thanks to implicit satisfaction of interfaces, Go has duck typing:

package duck

import (
    "otherPackage"
)

type Duck interface {
    Quack()
    Walk()
}

func playWithDuck(duck Duck) { ... }

func doSomeStuff() {
    mallard := otherPackage.GetMallard()
    // otherPackage does not “declare” Duck,
    // but Mallard has Quack() and Walk() methods
    playWithDuck(mallard)

but I miss implementation inheritance 🥺

Yes, I hear you, I hear you:

If we implemented Newtonian motion as the Update() for a super-class Foe, Bob and Chuck could inherit it. That would be very DRY.

But say you had this idea. Wouldn’t it be better to make the motion physics into a top-level library to be re-used, instead of burying it as part of a class hierarchy?

Let’s define some types for motion physics, for our game:

type MotionState struct {
    Position Vector3D
    Speed Vector3D
}

type LawOfMotion func (*MotionState, time.Duration) *MotionState

Assuming we had this in place, we might rewrite BobFoe like this:

type BobFoe struct {
    
    mState *MotionState
    move LawOfMotion
}

func (b *BobFoe) Update(dt) {
    
    b.mState = b.move(b.mState, dt)
}

With this approach, we could write support for not just Newtonian mechanics, but other kinds of LawOfMotion too:

func Newtonian(*MotionState, time.Duration) *MotionState {  }

func Relativistic(*MotionState, time.Duration) *MotionState {  }

func Brownian(*MotionState, time.Duration) *MotionState {  }

Bob and Chuck could use these as pluggable behaviors to be passed in as constructor parameters.

OO purists would have implemented LawOfMotion as a class hierarchy, and used the Strategy Pattern. But the functional approach is really much simpler.

How it feels

I think that not enough attention is paid in the industry to things that are a bit fuzzy and hard to quantify, like how a language feels. Instead, we see a lot of rationalizations based on supposedly scientific, fact-based criteria.

But if you talk informally to other programmers, non-measurables are often a factor. For example, having used Java during my three years at Amazon, my main objections to it are non-technical.

Just to say, I’m about to talk about feelings.

Go is a language with many fans, but it also has a vociferous crowd of haters that denounce that there are other languages with a more complete set of features, or that this or that kind of task is cumbersome to write in Go, for the lack of a particular feature in the language. Those people are right, but for lovers of Go, they miss the point.

Here is an excerpt from Alan Perlis’s foreword to the brilliant book Structure and Interpretation of Computer Programs:

Pascal is for building pyramids – imposing, breathtaking, static structures built by armies pushing heavy blocks into place. Lisp is for building organisms – imposing, breathtaking, dynamic structures built by squads fitting fluctuating myriads of simpler organisms into place.

The simple structure and natural applicability of lists are reflected in functions that are amazingly nonidiosyncratic. In Pascal the plethora of declarable data structures induces a specialization within functions that inhibits and penalizes casual cooperation.

One could think of paraphrasing this as a comparison between a classes-and-inheritance based OO language like C++ or Java, and Go with its interfaces. Building with the former languages feels heavy and complex. You start with class hierarchies, where a lot of low level implementation decisions need to be made at design time. Casual cooperation is penalized.

Go, in contrast, feels light. As light as could be hoped in a (mostly) strongly typed, compiled language. It’s not just thanks to interfaces. Go has a set of features that work well in combination.

As Rob Pike said:

If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.

It has often been remarked that a good programming language affects the way you think. Learning Go will help you think more clearly about objects. And I would be remiss not to mention Go’s channels and goroutines. They might revolutionize how you think of concurrency.


  1. watch Alan Kay’s OOPSLA 1997 talk for some of the historical background on messages and objects ↩︎

  2. Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma et al., Addison-Wesley, 1994 ↩︎

  3. not to mention the over-use of Design Patterns, another sad case of interesting ideas made into religion ↩︎

  4. in Go, most often, methods are defined on a struct , but a prominent example of a function type that has a method can be found in the standard library! (see the HandlerFunc type in net/http) “methods can be defined for any named type (except a pointer or an interface); the receiver does not have to be a struct.” refer to Effective Go ↩︎

Creative Commons License
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.