Type embedding in Go

October 6, 2020

What is type embedding?

You might already know that Go uses composition (has-a) instead of inheritance (is-a) to build reusable software components
and this is achieved using type embedding.

By including a type as a nameless field within another type, the exported members (fields and methods)
defined on the embedded type are accessible through the embedding type. A technique called “promotion”.

There are four valid ways to embed a type inside another one:

  1. Struct inside another struct as value
  2. Struct inside another struct as pointer
  3. Interface inside another interface
  4. Interface inside a struct

Embedding a struct in another struct as value field

type Human struct {
    name string
    birthDay time.Time
    age int
    location string
}
func (h Human) Eat() {
    fmt.Printf("%s is eating\n", h.name)
}
type Developer struct {
    Human
    remote bool
    fullStack bool
    mainProgrammingLanguage string
    codingSkills []string
    softSkills []string
}

The Developer type lists the Human type within the struct but does not give it a field name.
This allows direct access to the fields and methods defined on Human type within the Developer type
Another way to put it, the fields and methods from the Human type have been promoted to the Developer type
aka a Developer has a Human inside it.

var dev Developer
dev.name = "Leet Coder"
fmt.Println(dev.name)
dev.Eat()

If we were to give the embedded type a field name, then we would introduce another level of indirection
and lose the implicit access to the embedded type’s fields and methods.

type Developer struct {
    person Human
    remote bool
    fullStack bool
    mainProgrammingLanguage string
    codingSkills []string
    softSkills []string
}

var dev Developer
dev.name = "Gopher" // Error: dev.Name undefined (type Developer has no field or method Name)
dev.person.name = "Leet Gopher" 

Also the way you declare and initialize a struct with embedded types inside it matters.
In a struct literal, you have to use the embedded type as a field name when declaring and initializing variables with the := operator.

dev := Developer{
    Human: {
        name: "Allowed",
    }
}

instead of:

// Error: cannot use promoted field Human.Name in struct literal of type Developer
dev := Developer{
    name: "Not allowed",
}    

Embedding a struct in another struct as pointer field

type Entrepreneur struct {
    founder bool
    cofounder bool
    businessName string
}
type Developer struct {
    Human
    *Entrepreneur
    fullStack bool
    mainProgrammingLanguage string
    codingSkills []string
    softSkills []string
}

Sometimes a Developer can also be an Entrepreneur.
In such cases, we must initialize the Entrepreneur inside a Developer to point to a valid struct before using it.

dev := Developer{
    name: "Startup Developer",
    Entrepreneur: &Entrepreneur{
        founder: true,
        businessName: "LYMO - AI Robot Lawn Mower",
    },
}
fmt.Printf("Name: %s, Founder: %t, Business: %s\n", dev.name, dev.founder, dev.businessName)

If we try to use a promoted field from the Entrepreneur type inside a Developer with no entrepreneurial skills we get a panic,
SIGSEGV error: invalid memory address or nil pointer dereference

wannabeDev := Developer{
    Human: Human{
        Name: "Wannabe Developer",
    },
}
fmt.Println(wannabeDev.Founder)

We can however access the field to check if it is nil or not:

fmt.Println(wannabeDev.Entrepreneur)
or if wannabeDev.Entrepreneur != nil {
    fmt.Println(wannabeDev.Founder) // or fmt.Println(wannabeDev.Entrepreneur.Founder)
}

Embedding an interface inside another interface

Some key aspects about interfaces in Go:

  • Interfaces provide a way to specify the behavior of an object.
  • A type can implement multiple interfaces.
  • Interfaces are implemented implicity by a type using the concept of duck typing: “If it walks like a duck and it quacks like a duck, then it must be a duck!”
    Meaning, if a type has methods with the same signatures as all of the methods of an interface, it means that the type implements the interface
  • Only interfaces can be embedded within interfaces.
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type ReadWriter interface {
    Reader
    Writer
}

The ReadWriter interface has both Read and Write methods, “borrowed” from the Reader and Writer interfaces.

FileWrapper struct implements 3 interfaces: Reader, Writer, ReadWriter

package main

import "fmt"

type FileWrapper struct {
    path string
    isOpen bool
    file *os.File
}

func NewFileWrapper(filePath string) {
    return &FileWrapper{
        path: filePath,
    }
}

func (fw *FileWrapper) Open() error {    
    fw.file, err := os.Open(fw.path)
    if err != nil {
        return er
    }
    fw.isOpen = True
}

func (fw *FileWrapper) Close() {
    defer fw.file.Close()
}

func (fw *FileWrapper) Read(p []byte) (n int, err error) {
    return 0, fmt.Errorf("Not implemented yet")
}

func (fw *FileWrapper) Write(p []byte) (n int, err error) {
    return 0, fmt.Errorf("Not implemented yet")
}

func procesFile(f ReadWriter) {
   // reads data from file using f.Read()
   // process and modify data
   // write data back to file using f.Write()
}

func main() {
    bashrc := NewFileWrapper("/home/linux/bashrc")
    processFile(bashrc)
}

Embedding an interface inside a struct

A powerful construct you might not be aware of in Go is that you can embed an interface inside a struct.

An embedded interface inside a struct enables us to explicitly state that:

  • the embedding struct needs to satisfy the embedded interface

  • the data from the embedded type is hidden behind the interface

  • we can initialize the embedded interface with any struct that implements that interface.
    This means we can use the methods of another interface implementation inside this struct.

  • the methods from the other interface implementation passed in for the interface can be “overriden”
    by the embedding struct if desired or their implementation can be used as-is
    This is where the power comes from!
    We can “override” only a few methods we actually need in the context we are in and reuse the others!

Example 1

A great example of this concept can be seen in action in the sort package, where the sort.Interface is embedded inside the reverse unexported struct.

package sort

// ...

// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
	// Len is the number of elements in the collection.
	Len() int
	// Less reports whether the element with
	// index i should sort before the element with index j.
	Less(i, j int) bool
	// Swap swaps the elements with indexes i and j.
	Swap(i, j int)
}

type reverse struct {
	// This embedded Interface permits Reverse to use the methods of
	// another Interface implementation.
	Interface
}

// Less returns the opposite of the embedded implementation's Less method.
func (r reverse) Less(i, j int) bool {
	return r.Interface.Less(j, i)
}

// Reverse returns the reverse order for data.
func Reverse(data Interface) Interface {
	return &reverse{data}
}

// ...

// IntSlice attaches the methods of Interface to []int, sorting in increasing order.
type IntSlice []int

func (p IntSlice) Len() int           { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

// Sort is a convenience method.
func (p IntSlice) Sort() { Sort(p) }

// ...

// Sort sorts data.
// It makes one call to data.Len to determine n, and O(n*log(n)) calls to
// data.Less and data.Swap. The sort is not guaranteed to be stable.
func Sort(data Interface) {
	n := data.Len()
	quickSort(data, 0, n, maxDepth(n))
}
package main

import (
	"fmt"
	"sort"
)

func main() {
	s := []int{5, 2, 6, 3, 1, 4} // unsorted
	sort.Sort(sort.Reverse(sort.IntSlice(s)))
	fmt.Println(s)
}

Example 2

Embedding interfaces inside structs can be very useful when writing stubs/mocks/adapters for unit tests:

package main

import (
	"fmt"
)

type FileOps interface {
	Read()
	Write()
	Close()
}

type FileOpsImpl struct {
}

func (f FileOpsImpl) Read() {
	fmt.Println("Real read")
}

func (f FileOpsImpl) Write() {
	fmt.Println("Real Write")
}

func (f FileOpsImpl) Close() {
	fmt.Println("Real close")
}

type FileManager struct {
	FileOps
}

func NewFileManager(fileOps FileOps) *FileManager {
	return &FileManager{fileOps}
}

type FileOpsStub struct {
	FileOps
}

func (ops *FileOpsStub) Read() {
	fmt.Println("Mock read")
}

// TestFileRead only requires to Read from the adapter thus we only mock that method
func TestFileRead() {
   f := NewFileManager(&FileOpsStub{})
   f.Read() // prints: Mock read
   // f.Close() yields panic: runtime error: invalid memory address or nil pointer dereference
   // because FileOpsStub only implements Read() method
}

// TestFileReadWithClose "overrides" Read() method and "inherits" Write() and Close() from the underyling embedded interface implementation
func TestFileReadWithClose() {
   f := NewFileManager(&FileOpsStub{FileOpsImpl{}})
   f.Read() // prints: Mock read
   f.Close() // prints: Real close
}

func main() {
	TestFileRead()
	TestFileReadWithClose()
}

Example 3

Another example, where you can see that we override the Ride() method of the embedded interface inside Person struct to perform an action before starting to ride,
but p.Vehicle() is coming from Bike() struct which implements the Rider interface.

package main

import (
	"fmt"
)

type Rider interface {
    Vehicle() string
    Ride()
}

type Person struct {
    Rider
}

func (p Person) Ride() {
	fmt.Println("Person is preparing to ride a:", p.Vehicle())
	p.Rider.Ride() // super() like call
}

type BikeType string
const (
    MountainBike BikeType = "Mountain Bike"
    CrossCountry = "Cross Country"
    DownHill = "Down Hill"
    CityBike = "City Bike"
)

type Bike struct {
    Type BikeType
}

func (b Bike) Ride() {
    fmt.Printf("[%s 🚲ing] Type: %s!", b.Vehicle(), b.Type)
}

func (b Bike) Vehicle() string {
    return "Bike"
}

type Chair struct {
    Feet int
}

func main() {
    person := Person{Bike{MountainBike}, "Person 1"}
    person.Ride() 
    // prints:
    // Person is preparing to ride a: Bike
    // [Bike 🚲ing] Type: Mountain Bike!

    fmt.Println(person.Type) // error: person.Type undefined (type Person has no field or method Type)

    person2 := Person{Chair{4}}
    person2.Ride() // error: cannot use &Chair literal (type *Chair) as type Rider in field value: *Chair does not implement Rider (missing Ride method)
}

Conclusion

The Go type system enables code reusability through type embedding which promotes composition over inheritance.