OOP in traditional languages

OOP has 3 main areas that highlight its effectiveness, we can explain them under the assumption we have a superclass called Animal, and a subclass Dog that inherits the Animal class

  1. Code Reuse: a Dog would reuse code from Animal, preventing the need for code duplication and simplifying the process of refactoring.
  2. Polymorphism: the variable that is a type Dog can be treated as a Dog(subclass) or an Animal (superclass).
  3. Dynamic Dispatch: using functions of instances of Dog will determine if it is referring to a Dog’s method or Animal’s method.

OOP in Go

Go can achieve these same features through different means

  1. Composition & Embedding: give us code reuse
  2. Interfaces: give use Polymorphism and Dynamic Dispatch

Note: Don’t worry about type hierarchies when starting a new Go project – it’s easy to introduce polymorphism and dynamic dispatch later on because go does not use traditional inheritance based polymorphism.

Code Reuse by Composition

First off, go does not have classes, we call them structs, but it’s easy to think of them as a class that can have properties and methods.

If a subclass (sub struct) needs functionality from a different type, we can use composition to get that functionality.

type Animal struct {
	 some functionality
}

type Dog struct {
	// we can refer to Animal with a named property
	// I used beast, but you can call it anything
	beast  Animal		 //reference the animal struct
	 other dog things
}

Poof, this gives us any animal functionality with composition More in the weeds of how this works. Dog can be created with an Animal and it will obtain all of its properties.

package main

import "fmt"

// super struct
type Animal struct {
  Name string
  Age  int
}

// sub struct
type Dog struct {
  Animal
  BarkStrength int
}

func main() {
	// creating a dog, the first arg is an Animal
  dog := Dog{
	  // defining an Animal 
    Animal: Animal{
      Name: "Chole",
      Age:  1,
    },
    // defining other class properties
    BarkStrength: 10,
  }

  fmt.Printf("dog details: %#v\\n", dog)
}

Outputs:

dog details: main.Dog{Animal:main.Animal{Name:"Chole", Age:1}, BarkStrength:10}

The dog contains all the properties of Animal, but we created an animal when we created the dog, this is not necessary and go simplifies this with a shortened syntax (often called a sugared syntax, because its sweet to not type all this stuff out every time).

package main

import "fmt"

type Animal struct {
  Name string
  Age  int
}

type Dog struct {
  Animal
  BarkStrength int
}

func main() {
	// we can create the dog without declaring an Animal
  dog := Dog{
    BarkStrength: 10,
  }
  // Go will initilize all the Animal props with "zero" value
  // the zero value for string is "", an empty string
  // dog.Name would be and empty string
  // the zero value for int is 0
  // dog.Age would be 0
  dog.Name = "chloe"
  dog.Age = 1

  fmt.Printf("dog details: %#v\\n", dog)
}

Outputs:

dog details: main.dog{animal:main.animal{Name:"chloe", Age:1}, barkStrength:10}

We still manage to get all of the properties of animal without declaring a new animal type for every instance of dog. Go will handle that for us and initialize the Animal properties to the default Go zero values.

Code Reuse by Embedding

If you need to inherit exact behavior of functions then we need to use embedding. We can individually define all of the methods by representing the Animal property of Dog and re-type all of the methods that way.

type Animal struct {
	// any props we want to live in Animal
}
// all the animal functions
func (a *Animal) Eat()   {  }
func (a *Animal) Sleep() {  }
func (a *Animal) Breed() {  }

type Dog struct {
	beast Animal
	// any props we want to live in Dog
}
// defining the same functions for Dog
func (a *Dog) Eat()   { a.beast.Eat() }
func (a *Dog) Sleep() { a.beast.Sleep() }
func (a *Dog) Breed() { a.beast.Breed() }

This code pattern is known as delegation, Dog has implementations of the same methods as Animal thus it can delegate when we call those methods.

This is a lot to type out every time we want to inherit functionality, thankfully go has some more sugared syntax. Go can reduce that code with Embedding, and those retyped methods referencing Animal can be reduced to:

type Animal struct {
	// any props we want to live in Animal
}
// all the animal functions
func (a *Animal) Eat()   {  }
func (a *Animal) Sleep() {  }
func (a *Animal) Breed() {  }

type Dog struct{
	Animal
}
// Dog now has access to all the animal functions

This will give us the same methods that Animal has for every Dog. The specific implementation details live in Animal.

Polymorphism and Dynamic Dispatch in Go

Go provides interface types like many other languages, and we can use those languages to abstract over concrete objects and implementation details.

Rule of thumb: Keep your interfaces short, and introduce them only when needed.

If the number of structs that use Animal ever increases, then we will want to use interfaces to give us polymorphic and dynamic dispatch behavior.

Lets say all of our animals need to sleep, then we can build an interface to abstract away that behavior. Pretend we have Dog and Cat , that are Animal’s. They are a part of another package (meaning we didn’t write their code, but we want to use their sleep methods but let which method be determined at run time. We can build an interface to abstract that logic away:

// define an interface with a sleep method
type Sleeper interface {
	Sleep()
}

func main() {
	// create an array of things that implement that interface
	pets := []Sleeper{new(Cat), new(Dog)}
	// iterate over our array
	for _, x := range pets {
		// for every thing that implements our interface.
		// if Cat and Dog have sleep methods, then at runtime
		// the correct method will be called
		x.Sleep()
	}
}

The interface does not really care what type of thing is going its is going to wrap, just that that struct has a .Sleep() method on it. Unlike other languages where we would have to explicitly delcare that a struct implements a specific interface, Go does not require this explicit “implements” declaration.

Note: The above code is actually something called the ADAPTER pattern, or wrapper class. We have built an adaptive way to handle polymorphism over objects that we did not build, and our program can adapt to different concrete implementations of those objects.

Constructors

By default all structs in go have zero value initializations for properties. this means if you create an instance of your struct, all its value default to their zero value. We touched on this briefly with the sugared syntax for embedding.

// A StopWatch is a simple clock utility.
// Its zero value is an idle clock with 0 total time.
type StopWatch struct {
    start   time.Time
    total   time.Duration
    running bool
}

var clock StopWatch // Ready to use, no initialization needed.

If this isn’t enough then you have to use the structs factory functions to initialize the value to something more useful. Examples of factory functions are .NewScanner() and .New().

scanner := bufio.NewScanner(os.Stdin)
err := errors.New(Houston, we have a problem)

Factory functions are static intilization methods that belong to objects. Lets build our own example of a factory function.

func NewFile(id int, name string) *File {
    if id < 0 {
        return nil
    }
    // use new() to declare a new object of type File
    // don't worry, we will talk about new() in just a second
    f := new(File)
    // properties of File we need to initialize
    f.fd = id
    f.name = name
    f.dirinfo = nil
    f.nepipe = 0
    return f
}

// we can call this function like so

ourNewFile := NewFile(7, "test.text")

Composite literals to simplify code

Sugared syntax to the resume again, we can reduce this code thanks to composite literals, which reduce an expression at evaluation in Go.

func NewFile(fd int, name string) *File {
    if fd < 0 {
        return nil
    }
    // Composite literall
    f := File{fd, name, nil, 0}
    return &f
}

It’s perfectly OK to return the address of a local variable; the storage associated with the variable survives after the function returns. In fact, taking the address of a composite literal allocates a fresh instance each time it is evaluated, so we can combine these last two lines.

return &File{fd, name, nil, 0}

The fields of a composite literal are laid out in order and must all be present. However, by labeling the elements explicitly as {field: value} pairs, the initializers can appear in any order, with the missing ones left as their respective zero values. Thus we could say return &File{fd: fd, name: name}, much like how javascript handles object properties.

Note: As a limiting case, if a composite literal contains no fields at all, it creates a zero value for the type. The expressions new(File) and &File{} are equivalent.

new(foo) vs make(foo)

  1. new(foo) The new() function will allocate memory for the desired object, and initialize its properties to default zero values. All three of these functions are adequate and equivalent to achieve the same thing, which is a buffer with default zero values.
// Allocate enough memory to store a bytes.Buffer value
// and return a pointer to the value’s address.
var buf bytes.Buffer
p := &buf

// Use a composite literal to perform allocation and
// return a pointer to the value’s address.
p := &bytes.Buffer{}

// Use the new function to perform allocation, which will
// return a pointer to the value’s address.
p := new(bytes.Buffer)
  1. make(foo) The make()function, on the other hand, is a special built-in function that is used to initialize slices, maps, and channels. make() can only be used to initialize slices, maps, and channels, and that, unlike the new() function, make() does not return a pointer.
// Using make() to initialize a map.
m := make(map[string]bool, 0)

// Using a composite literal to initialize a map.
m := map[string]bool{}

// Non zero values with composite literal
m := map[string]bool{
    java: false,
    go:   true,
}

Closing Notes

Go tries to stray away from traditional Object Oriented Programming, and instead offers new ways of thinking about how objects behave and interact with each other. Use these tools to overcome the situations where you need OOP traits. ✌️