For those coming from an Object Oriented language background to Go, it can be rather jarring to hear that there are no classes and thus no inheritance in Go. While this is strictly true, there are ways to "inherit" the same behavior and promote more code reuse in Go.

Before we get started, it's important to understand why Go doesn't have inheritance - by design - and the problems that classical inheritance can create. We'll first approach a situation with inheritance and gradually move toward composition.

Basic Inheritance

The benefits of inheritance are mainly code reuse, polymorphism, and dynamic dispatch. While these are valid objectives, there's more than one way to achieve them.

One of the most common examples of inheritance actually illustrates some of the problems with the approach.

In Java this relationship can be expressed clearly with the implements keyword.

public interface Animal {
    Sound makeSound()
}

public class Dog implements Animal {
    /* implementation */
}

public class Cat implements Animal {
    /* implementation */
}

As we can see in the the diagram above, a Dog IS-A Animal, and the same goes for Cat. There are some problems here though.

  • The abstraction - and the basis for our mental model - is faulty, because not all animals make a sound. This is something we may not consider up front and will lead to segregating the interface later or reworking the inheritance chain in the worst case.
  • Similarly, each of our implementing classes has a wagTail() method that is inaccessible through the interface, so we would have to break the abstraction to call it. This makes the abstraction rather leaky because we're not cleanly separating implementation from use.

While both of our Animal implementations have a wagTail() method, not all Animals have tails, so we can't just hoist up the method declaration into the Animal interface.

Interface Segregation

One way to address these problems is to separate the interface up front. It allows for more flexibility in how we represent functionality, and one class can implement multiple interfaces.

public interface SoundMaker {
    Sound makeSound()
}
public interface TailWagger {
    void wagTail()
}
public class Dog implements SoundMaker, TailWagger {
    @Override
    public Sound makeSound() {
        return new Bark();
    }

    @Override
    public void wagTail() {
    }
}

This can be done in Go too. These two snippets are pretty much equivalent. The method names are capitalized in the Go version to expose them for use to code outside the current package.

type SoundMaker interface {
    MakeSound() Sound
}
type TailWagger interface {
    WagTail()
}

type Dog struct {
}
func (d Dog) MakeSound() Sound {
    return "Ruff!"
}
func (d Dog) WagTail() {
}

You might be wondering how we know that Dog implements the interface. Go uses "duck typing" to determine whether a type implements a given interface. So there's really no need for an implements keyword.

Looks like a duck, quacks like a duck, must be a duck.

This means that the compiler can't enforce whether a type implements an interface until we try to assign it to one. This is a common pattern to enforce this up front.

var _ SoundMaker = (*Dog)(nil)
var _ TailWagger = (*Dog)(nil)

This will simulate the assignment, but ignore the resulting variable declaration since the go compiler throws errors for unused variables.

Note:

Go interfaces do not encapsulate data, only operations. This is a key difference between the two languages.

A Quick Aside

There's a small convention clash between how Java interfaces are named vs. Go. In Java these would be names something like Soundable or Waggable. In Go, the common way to name interfaces is as shown. An example of this is the Stringer interface (see the docs).

Code Reuse

This is where we are so far.

In the case of Dog and Cat, they don't sound alike, nor do they wag their tail in exactly the same way. With many different types of Animals, it could get really tedious to have to write the same code over and over again for similar animals with slight differences in implementation. Since Go doesn't allow locked in class hierarchies, we can extend and reuse this functionality by embedding a type in another.

Here we can reuse the common aspects of Dog and Cat to create specific variants of them. Again, this is not inheritance, but embedding. What's happening under the covers is that Corgi and Persian are given matching methods of Dog and Cat that delegate to the underlying implementation.

var _ SoundMaker = (*Corgi)(nil) // No error here
var _ TailWagger = (*Corgi)(nil) // Still good
type Corgi struct {
    Dog
}
func (c Corgi) BeSuperCute() {
}

This is effectively the code that is added onto the type by embedding Dog in Corgi. Note that this does not have to be written by us, we get it by embedding the existing struct. This makes the embedding and delegation mechanism pretty convenient.

func (c Corgi) MakeSound() {
    return c.Dog.MakeSound()
}
func (c Corgi) WagTail() {
    c.Dog.WagTail()
}

Since the MakeSound and WagTail methods exist for the Corgi type, it satisfies the requirement to implement the respective interfaces.

Composition

The example above doesn't get us away from the fragile base class problem because we're still forcing the abstract base class model on the language. This also means we haven't really solved the abstraction problems. There is still a tightly coupled base implementation that requires more of the lower level functionality than what might make sense.

This is where we start moving more toward composition of functionality versus inheritance-like coupling, all while providing different implementations behind abstract interfaces. Let's start with some examples of components which can be embedded to satisfy these interfaces.

type HissSoundMaker struct {}
func (h HissSoundMaker) MakeSound() Sound {
    return "Hisss!"
}

type BarkSoundMaker struct {}
func (b BarkSoundMaker) MakeSound() Sound {
    return "Arf!"
}

type MeowSoundMaker struct {}
func (m MeowSoundMaker) MakeSound() Sound {
    return "Meow!"
}

type InsubordinateTailWagger struct {}
func (i InsubordinateTailWagger) WagTail() {
    fmt.Println("...No.")
}

type HappyTailWagger struct {}
func (h HappyTailWagger) WagTail() {
    fmt.Println("I'm wagging my tail right now!")
}

Now we can construct many different animals all from different building blocks. Ones with or without tails, animals that may or may not make noise, and functions can make assertions about what it requires without ever knowing about or relying upon the details of how tails and sounds work under the covers.

// We can use composition with interfaces too!
type NoisyTailWagger interface {
    SoundMaker
    TailWagger
}

var _ NoisyTailWagger = (*Corgi)(nil)
type Corgi struct {
    BarkSoundMaker
    HappyTailWagger
}

var _ NoisyTailWagger = (*Persian)(nil)
type Persian struct {
    HissSoundMaker
    InsubordinateTailWagger
}

var _ NoisyTailWagger = (*Rattlesnake)(nil)
type Rattlesnake struct {
    HissSoundMaker
    HappyTailWagger
}

// Lizard doesn't make noise!
// var _ NoisyTailWagger = (*Gecko)(nil)
var _ TailWagger = (*Lizard)(nil)
type Lizard struct {
    HappyTailWagger
}

// Completely valid slice of NoisyTailWaggers
var animals = []NoisyTailWagger{Corgi{}, Persian{}, Rattlesnake{}}
func PrintAnimals() {
    for _, a := range animals {
        fmt.Printf("Animal says %s. Wag your tail, please.", a.MakeSound())
        a.WagTail()
    }
}

Notice that we have Lizard which has a tail but doesn't make noise, so it doesn't make sense to force a Lizard to implement SoundMaker. The compiler helps protect us from ourselves and prevents us from accidentally adding it in the list of animals at the bottom of the snippet.

Conclusion

Hopefully this has provided some food for thought about how to structure complex structures in more maintainable and testable ways (you are testing, right). This also accomplishes the same goals of object oriented class hierarchies without getting locked into that rigid view. Composition can make building up complex families of structures easier by creating building blocks first, then putting them all together to expose more complex implementations. This allows a system to be more flexible and maintainable by reducing the impact of change and providing better abstractions.

Here are some guidelines for moving toward composition.

  • Interfaces should be small. If many interfaces are needed to make it clear what is required, embed them together into a larger interface.
  • Resist creating complex "inheritance" chains, as this can makes the system more rigid and harder to change without breaking things.
  • Think of composition as types "opting in" to functionality as opposed to being bound to a complex contractual obligation. With small interfaces, actual implementation requirements are lighter and easier to change as necessary.