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:
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",
}
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)
}
Some key aspects about interfaces in Go:
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)
}
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!
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)
}
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()
}
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)
}
The Go type system enables code reusability through type embedding which promotes composition over inheritance.