Go and Object-Oriented Programming

Oct 31, 2020

james-harrison

When I first worked with Go during school, I thought it was very similar to a traditional object-oriented language. It’s not. Go has many similarities with object-oriented langauges, however, when you take a look at the details, you’ll see that it’s a mixture of programming paradigms which will change the way how you design object-oriented Go applications.

I am assuming that you already know what the foundational principles of OOP are in this post. If you need some refreshing materials, check out this introduction from tutorialspoint.

Abstraction & Encapsulation

Let’s start off with the concepts that are easier to see in Go: abstraction and encapsulation. I’m grouping both together because they’re commonly used in conjunction with one another in service designs. For example, consider the following database service snippet.

// Entry represents a sample database entry
type Entry struct {
    // Entry fields...
}

// DatabaseEntry is the public entry point to database methods
type DatabaseEntry interface {
    FindByID(id int) (*Entry, error)
}

type databaseService struct {
    DatabaseEntry
}

type database struct {
    DatabaseEntry
}

// NewDatabaseService creates an entry point to the database implementation
func NewDatabaseService() DatabaseEntry {
    return &databaseService{
        DatabaseEntry: &database{},
    }
}

// FindByID returns an Entry in the database given an ID
func (d *database) FindByID(id int) (*Entry, error) {
    // Finding entry by ID...
}

// Private function specifically related to database type
func (d *database) secretDatabaseMethod() {
    // Do something private here
}

The example above has one entry point into the database service, namely through NewDatabaseService(). The function returns any object which implements the DatabaseEntry interface. In our case, we embed an inner struct implementing the DatabaseEntry methods, databaseService, and set its interface implementation to database. This method works because database is another struct which implements the DatabaseEntry interface.

Using this nested structure gives us three main benefits:

  1. Encapsulate private database operations such as secretDatabaseMethod()
  2. Abstract database operations - secretDatabaseMethod can be a helper function that doesn’t clutter our main logic
  3. Adding layers is easy and will also be in its own encapsulated logic

For example, adding a validation layer can be as easy as the following:

func NewDatabaseService() DatabaseEntry {
    return &databaseService{
        DatabaseEntry: &databaseValidate{
            DatabaseEntry: &database{},
        },
    }
}

That being said, a more general form of encapsulation can be performed on a package-wide level using exported and unexported methods/declarations. Exported functions/declarations are public to other packages and are represented by a capitalized first letter. Unexported functions/declarations are only available by methods in the same package and are characterized by a lowercase first letter.

package example

// available in all packages
func ThisIsExported() {...}

// only available in the example package
func notExported() {...}

Composition (Inheritance?)

Inheritance is where Go differs from traditional object-oriented languages. Go has no inheritance!

Everything in Go is represented by structures rather than classes. The designers of Go intended to reduce dependency on class relationships because it usually led to large and complex object interactions. This, however, does not mean that we can’t relate objects together in Go.

Go solves the inheritance problem with composition. This basically means that objects in Go can be put together using different objects.

Suppose we try to represent smartphones with a specific company. Both the concept of smartphone and company can be split into two separate objects since they’re fundamentally different, but by using composition, we can attribute a specific smartphone with a company.

type Company struct {
    Smartphone
}

type Smartphone struct {
    name string
}

This particular technique shown above is also called struct embedding.

Composition is also compatible with interfaces. In other words, we can embed interfaces within each other as well.

type Company struct {}

type Cell struct{}

// CompanyData is an interface which is built from two other interfaces, Print and Cellphone
type CompanyData interface {
    Print
    Cellphone
}

type Print interface {
    printCompanyName()
}

type Cellphone interface {
    getCell() *Cell
}

func (c *Company) printCompanyName() {
    fmt.Println("Apple")
}

// func (c *Company) getCell() *Cell {
//  return &Cell{}
// }

// sanity check which forces errors when interfaces are not implemented properly
// this line should throw errors since getCell() is commented out
var _ CompanyData = &Company{}

Composition’s greatest advantage over inheritance is that it’s much easier to add or delete functionality without breaking other object dependencies. Plainly speaking, don’t worry about finding similarities between the keyboard and screen on a laptop.

Polymorphism

Our discussion on composition is a good transition into polymorphism. Go makes it easy to work with polymorphic methods despite the absence of inheritance.

Go distinguishes methods for specific types based on the receiver type specified in the function declaration. For example, the function, getName(), handles objects of type Fruit.

type Fruit struct {
    name string
}

// This function will return the name of Fruit objects
func (f *Fruit) getName() string {
    return f.name
}

We can also define getName() for different objects such as drinks like so…

type Drink struct {
    name string
}

// This function will return the name of Drink objects
func (d *Drink) getName() string {
    return d.name
}

type Fruit struct {
    name string
}

// This function will return the name of Fruit objects
func (f *Fruit) getName() string {
    return f.name
}

We just achieved polymorphism!

Let’s go over a harder example.

Consider the following situation where we want to calculate the total reading time of a book and magazine (or even multiple books and magazines). Both the book and magazine are represented as structs with two fields - name and page count. The reading count is calculated in readingTime(). As you can see (below), readingTime() is a polymorphic function since we have one defined for both Books and Magazines.

Now we’ve hit some problems:

  1. How can we write a method which captures both Book and Magazine types?
  2. How can we generalize the method to accept multiple Book and Magazine objects?
  3. Is there a way to just capture readingTime? For instance, Book has a getName method and Magazine has a requiresSubscription method which don’t overlap with each other

It turns out that Go has a very neat solution for these problems! What we can do is to create a general function which defines an inline interface that accepts multiple objects which implement the readingTime method. This allows us to collect object types that has a reading time associated with it. There’s also an additional benefit in which the inline scope only captures our specified method. Therefore, the function getReadTime filters out unrelated methods from objects (getName from Book and requiresSubscription from Magazine are not accessible).

package main

import "fmt"

type Book struct {
    name      string
    pageCount int
}

func (b *Book) readingTime() int {
    readTime := 10 * b.pageCount
    return readTime
}

func (b *Book) getName() string {
    return b.name
}

type Magazine struct {
    name      string
    pageCount int
}

func (m *Magazine) readingTime() int {
    return 5 * m.pageCount
}

func (m *Magazine) requiresSubscription() bool {
    return true
}

func getReadTime(r ...interface {
    readingTime() int
}) int {
    time := 0
    for _, item := range r {
        time += item.readingTime()
    }
    return time
}

func main() {
    book := &Book{
        name:      "A Dance With Dragons",
        pageCount: 1051,
    }
    mag := &Magazine{
        name:      "Brewing For Coffee Lovers",
        pageCount: 30,
    }
    fmt.Printf("Total read time: %d\n", getReadTime(book, mag))
}

Helpful Resources

Interested in Go and want to learn more? I find the following good reads!

Yiping Su
Yiping Su
Software Engineer

I am interested in data, software engineering, and the application of computer science concepts in real-world scenarios.

Related