The Struct Embedding Trap in Go

In 2019, when I decided to learn Go, I opted, as I usually do, to implement a backend to generate SableCC parsers in Go. I learned the language relatively quickly because of its similarity to Pascal and C. Nonetheless, while trying to write object-oriented code, I ended up falling into a trap when implementing inheritance.

Go doesn’t necessarily support inheritance, but it offers a feature known as type embedding, and this is where one falls into the inheritance trap, because it feels like inheritance.

I’ve been wanting to share this through an article, but with so much on my plate, I ended up postponing it. However, when I noticed a programmer fall into the same trap in an article he wrote about Struct Embedding in Go, I decided it was time to alert others about this trap. In fact, I have been noticing more and more programmers wrongly making the same assumption.

So, without further ado, let’s dive in and clarify what type embedding is, and how one can avoid what I call the inheritance trap.

Let’s begin with an example of inheritance in Java. More specifically, let’s use the inheritance in the following diagram:

Inheritance Diagram

This is a typical single inheritance, and in Java we implement it like this:

public class Base {
  private message;

  public Base(String message) {
    this.message = message;
  }

  public void printMessage() {
    // method code goes here
  }

  public void sayHi() {
    // method code goes here
  }
}

public class Derived extends Base {
  public Derived(String message) {
    super(message);
  }
}

Here, Derived inherits all the attributes and all the methods of the Base class. What this means is that by executing the following code:

Derived d = new Derived("This is a subclass");
d.printMessage();
d.SayHi();

we can take advantage of inheritance by reusing code from the Base class. Quite convenient and productive.

In Go, we can achieve a similar behaviour with type embedding, or more specifically Struct Embedding, which uses composition. But, before we look into Struct Embedding, let’s first see how we would do it without it.

Let’s convert the previous diagram into one that uses composition like so:

Composition Diagram

The code in Go is the following:

type Base struct {
  message string
}

func (b Base) PrintMessage() {
  // code goes here
}

func (b Base) SayHi() {
  // code goes here
}

type Derived struct {
  b Base
}

// now we implement the methods again in Derived
func (s Derived) PrintMessage() {
  // this emulates our inheritance
  s.b.PrintMessage() 
}

func (s Derived) SayHi() {
  s.b.SayHi()
}

and then, if we wanted to use it, we could do it as follows:

func main() {
  s := Derived{
    b: Base{
      message: "This is the message",
    },
  }
  s.PrintMessage()
  s.SayHi()
}

This looks like how one would implement it in C, and it certainly isn’t an elegant solution, not to mention efficient.

Go was designed for productivity. Therefore, there must be a more productive way of achieving the same result. In fact, there is, and it is known as type embedding, or more specifically struct embedding, something we’ve previously mentioned.

The code using struct embedding looks like the following:

type Base struct {
  // as before
}

// all Base methods as before

// I am using Derived just to follow
// the comparison with Java.
// In practice, you would give it a more meaningful name
type Derived struct {
  // Embed the base struct
  Base
}

and now, in the main function we can call it as before:

func main() {
  d := Derived{
    Base: Base{
      message: "This is the message",
    },
  }
  d.PrintMessage()
  d.SayHi()
}

As you can see, we didn’t have to write any code for the Derived struct, besides embedding the Base struct. It all seems automatic, but behind the hood, it is just syntactic sugar for the version we’ve seen earlier.

At first glance, this feels easy and without any pitfalls. However, if, in the Derived struct, we overwrite a method that is called from another method that exists only in the Base struct, this is where we get into trouble. And to better see this in practice, let’s write a concrete example, but this time overwriting the SayHi() method in the Derived struct as follows:

import "fmt"

type Base struct {
  message string
}

func (b Base) PrintMessage() {
  fmt.Println(b.message)
  b.SayHi()
}

func (b Base) SayHi() {
  fmt.Println("Say Hi from Base")
}

type Derived struct {
  Base
}

func (s Derived) SayHi() {
  fmt.Println("Say Hi from Subclass")
}

By implementing the following driver code:

func main() {
  b := Base{message: "Message of Base"}}
  d := Derived{Base: Base{message : "Message of Derived"}}

  // now we call the method PrintMessage on each instance
  b.PrintMessage()
  d.PrintMessage()
}

we would expect the following output:

Message of Base
Say Hi from Base
Message of Derived
Say Hi from Subclass

However, executing the program, we will get the following output instead:

Message of Base
Say Hi from Base
Message of Derived
Say Hi from Base

Give it a try here.

What happened here? You may ask.

To answer this question let’s look closely into the code. More specifically, let’s zoom into the PrintMessage method:

func (b Base) PrintMessage() {
  fmt.Println(b.message) // this is fine
  b.SayHi() // here is the problem
}

Looking at the line where the SayHi method is called, it is notable that the method call is associated with an instance of the Base struct. That is, the b receiver. This is not what we want. We want to call this method from an instance of Derived.

Coming to this realization, it may occur to us that we need to overwrite the PrintMessage method in the Derived struct as well. But this would require us to write more code. And this isn’t what we want, is it? Thankfully, Go offers another feature that allows us to shorten the code: interfaces.

By following the example of interface embedding from here, we create an interface that defines the method(s) that will be overwritten, and then we use type embedding, but this time with an interface instead of a struct like so:

import "fmt"

// here we have the interface with the method
// that will be overwritten,
// named after the method itself.
// this is Go's convention for single method interfaces,
// but I'll leave that for another tutorial
type HiSayer interface {
  SayHi()
}

// Base now embedds HiSayer
type Base struct {
  HiSayer // this will allow us to emulate the polymorphism
  message string
}

// returns a new instance of Base
// this is a utility function to save us from
// having to assign the instance to the interface
// it, or the embedding struct embeds
func CreateBase(message string) Base {
  b := Base{message: message} // ordinary instantiation
  b.HiSayer = b // this will be used to call the SayHi method
  return b
}

// and now we modify PrintMessage to call HiSayer.SayHi
// so all structures that implement the HiSayer interface
// will have the SayHi method
func (b Base) PrintMessage() {
  fmt.Println(b.message)
  // now the SayHi of the concrete instance 
  // assigned to HiSayer will be called
  b.HiSayer.SayHi()
}

// type Derived does not change
type Derived struct {
  Base
}

// returns a new instance of Derived
// another utility function
func CreateDerived(message string) Derived {
  d := Derived{
    Base: Base{
      message : message,
    },
  }
  d.HiSayer = d // assing itself to the HiSayer field
  return d
}

We wrote a little more code than we did in Java, but in the end it payed off, and we are able to emulate inheritance. The full code can be found here if you are interested.

On the other hand, if you would like to learn more about type embedding I suggest you read Bruno Scheufler’s article, or Ralph Ligtenberg’s one.

We’ve reached the end of the article, which I hope helped demystify the inheritance trap many fall into when starting to program in Go.

If after reading this article you still have questions, please let me know by commenting on my social networks, or by contacting me directly. I always look forward to learning from alternative solutions. So, please, don’t be shy and share it with me.

Related Posts:

  • Why I Chose Golang Over Rust In 2019 I was looking for a new, modern programming language to replace Java as my main language. I needed to rewrite an interpreter in a faster programming language as Java wasn’t quite working any more. My options were Rust, Go and Swift, from which I shortlisted Rust and Go. Read More
  • Chandler Carruth Announces Carbon Chandler Carruth just announced today a new programming language called Carbon, an experimental language that is supposed to be the successor of C++. Read More