Object Oriented Programming in Go
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
- Code Reuse: a
Dog
would reuse code fromAnimal
, preventing the need for code duplication and simplifying the process of refactoring. - Polymorphism: the variable that is a type
Dog
can be treated as aDog
(subclass) or anAnimal
(superclass). - Dynamic Dispatch: using functions of instances of
Dog
will determine if it is referring to aDog
’s method orAnimal
’s method.
OOP in Go⌗
Go can achieve these same features through different means
- Composition & Embedding: give us code reuse
- 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)⌗
new(foo)
Thenew()
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)
make(foo)
Themake()
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 thenew()
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. ✌️