Basic OOP and Composition in Go

I have been studying the Go programming language for several weeks now and thought about writing a series of posts to share what I have learned so far. I figured that it will be an excellent way to reinforce my understanding of the language. I initially thought about writing a post that will discuss concurrency in Go but it turned out that I am not yet eloquent enough to talk about basic concurrency patterns with goroutines and channels. I decided to set the draft aside and write about something I am more comfortable with at the moment: basic object-oriented patterns and composition in Go.

One of the best things I like about Go is its terseness. It made me realize that being advanced does not necessarily need to be complex. There are only a few reserved words, and just going through some of the basic data structures will enable you to read and comprehend most Go projects at Github. In fact, Go is not an object oriented language in the purest sense. According to the Golang FAQ:

Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

If Go is not an object-oriented language and everyone is going crazy about Functional Programming in the web development world, then why bother learning OOP patterns in Go? Well, OOP is a widely taught paradigm in CS and IT curricula around the world. If used correctly, I still believe that object-oriented patterns still have its place in modern software development.

Using structs

Go does not have a class similar to a real object-oriented language. However, you can mimic a class by using a struct and then attaching functions to it. The types defined inside the struct will act as the member variables, and the functions will serve as the methods:

package main

import "fmt"

type person struct {  
  name string
  age  int
}

func (p person) talk() {  
  fmt.Printf("Hi, my name is %s and I am %d years old.\n", p.name, p.age)
}

func main() {  
  p1 := person{"John Crisostomo", 25}
  p1.talk()
  // prints: "Hi, my name is John Crisostomo and I am 25 years old."
}

Run this code

On our example above, we have declared a type struct called person with two fields: name and age. In Go, structs are just that, a typed collection of fields that are useful for grouping together related data.

After the struct declaration, we declared a function called talk. The first parenthesis after the keyword func specifies the receiver of the function. By using p of type person as our receiver, every variable of type person will now have a talk method attached to it.

We saw that in action on our main function where we declared and assigned p1 to be of type person and then invoking the talk method.

Overriding methods and method promotion

A struct is a type, hence, it can be embedded inside another struct. If the embedded struct is a receiver of a function, this function gets promoted and can be directly accessed by the outer struct:

package main

import (  
    "fmt"
)

type creature struct {}

func (c creature) walk() {  
  fmt.Println("The creature is walking.")
}

type human struct {  
  creature
}

func main() {  
  h := human{
    creature{},
  }
  h.walk()
  // prints: "The creature is walking."
}

Run this code

We can override this function by attaching a similarly named function to our human struct:

package main

import (  
    "fmt"
)

type creature struct {}

func (c creature) walk() {  
  fmt.Println("The creature is walking.")
}

type human struct {  
  creature
}

func (h human) walk() {  
  fmt.Println("The human is walking.")
}

func main() {  
  h := human{
    creature{},
  }
  h.walk()
  // prints: "The human is walking."
  h.creature.walk()
  // prints: "The creature is walking."
}

Run this code

As we can see on our contrived example, the promoted method can easily be overridden, and the overridden function of the embedded struct is still accessible.

Interfaces and Polymorphism

Interfaces in Go are used to define a type's behavior. It is a collection of methods that a particular type can do. Here's the simplest explanation I can muster: if a struct has all of the methods in an interface, then it can be said that the struct is implementing that interface. This is a concept that can be easily grasped through code, so let us make use of our previous example to demonstrate this:

package main

import (  
    "fmt"
)

type lifeForm interface {  
   walk()
}

type creature struct {}

func (c creature) walk() {  
  fmt.Println("The creature is walking.")
}

type human struct {  
  creature
}

func (h human) walk() {  
  fmt.Println("The human is walking.")
}

func performAction(lf lifeForm) {  
  lf.walk()
}

func main() {  
  c := creature{}
  h := human{
    creature{},
  }

  performAction(c)
  // prints: "The creature is walking."
  performAction(h)
  // prints: "The human is walking."
}

Run this code

In this modified example, we declared an interface called lifeForm which has a walk method. Just like what we have discussed above, it can be said that both creature and human implements the interface lifeForm because they both have a walk method attached to them.

We also declared a new function called performAction, which takes a parameter of type lifeForm. Since both c and h implements lifeForm, they can both be passed as an argument to performAction. The correct walk function will invoked accordingly.

Wrap up

There is so much more to object-oriented programming than what we have covered here but I hope it is enough to get you started in implementing class-like behavior with Golang's structs and interfaces. On my next post, I will talk about goroutines, channels and some basic concurrency patterns in Go. If there's something you would like to add up to what I have covered here, please feel free to leave a comment.

Author

John Crisostomo

Software Engineer, currently interested in React, React Native and GraphQL