6 min read

Go: Second Life of OOP

How hard is it to switch from the classic Java-style objects to Go? 🤔 And do we really need one more Adapter Decorator Builder Factory?

Greetings, most excellent fellow developer!

I’ve been writing code in Java, Python and Perl professionally, but I’ve recently discovered Go and I’d like to experiment with it and see what works and what doesn’t 🤓

You’ve probably seen Go as well, it’s different in its OOP approach.

a business man in sunglasses waving his hand in front of his face and the text that says “same, but different, but still same”


Is Go Object Oriented?

First of all, let’s agree on what we’re looking for:
At its core, an object is some data (a block of memory) with attached logic(behavior) that can access that data.

This is exactly what Go provides and this isn’t exactly new. Go seems to draw inspiration from (or from the same sources) as Perl that also has package-level entities and logic attached to existing structural primitives (hash tables, lists etc) rather than full-fledged classes.

Although it’s out of the scope of this post, but I have noticed that this is one of many parallels between Go and Perl and it’s fascinating to me how an effectively dead language has managed to influence one of the fastest growing language communities now. 🧐

Let’s now look at how we can define a new object type:

package main

import "fmt"
import "strings"

// this is a type alias that gives a name to a specific data structure
type StringTable struct {
    values [][]string
}

// logic is defined outside of the type declaration, methods are attached to an existing type
func (s StringTable) NoOp() { }

// for example, let's define a readable CSV representation for the StringTable
func (s StringTable) String() string {
    lines := make([]string, 0)
    for _, row := range s.values {
        lines = append(lines, strings.Join(row, ","))
    }
    return strings.Join(lines, "\n")
}

func main() {
    // initialize the object
    table := StringTable { values: [][]string{
        []string{ "A", "B", "C" },
        []string{ "1", "2", "3" },
    } }

    // call a method explicitly
    table.NoOp()

    // String() is called implicitly
    fmt.Println(table)
}

This outputs our table data in a readable CSV format that we can feed into Excel, Python or any other application:

A,B,C
1,2,3

Try and run it yourself in Go Playground

Pillars of OOP: Encapsulation

This pillar is all about controlling access to the enclosed (encapsulated) data in an object. Traditionally, languages use private and public (and sometimes protected) field modifiers to set access and visibility.

a cat swirling in a large glass jar

In Go access and visibility works on a package level and follows a convention of uppercase names for public (visible outside of the current package) and lowercase for private (visible within the package).

So, let’s iterate on the earlier example and extract StringTable type into a separate package to see how this works!

First, we’ll need to run go mod init example to initialize a project in the current directory and be able to import other modules under the same path.

Then, we’ll break the code into two files:

main.go:

package main

import "fmt"
import "example/stringtable"

func main() {
    // initialize the object
    table := stringtable.NewStringTable([][]string{
        []string{ "A", "B", "C" },
        []string{ "1", "2", "3" },
    })

    // here we cannot access values directly
    // table.values would not work
    // but we can use a function from the stringtable package that has access
    stringtable.ExtractValues(table)

    // String() is called implicitly
    fmt.Println(table)
}

stringtable/table.go:

package stringtable

import "strings"

// note that StringTable starts with a capital, so it's visible outside of this package
type StringTable struct {
    // this field though is package-private and we cannot use it directly in our main anymore
    values [][]string
}

// we need a new constructor function to get an object with the values set
func NewStringTable(values [][]string) StringTable {
    return StringTable{ values: values }
}

// as before, this method can still access the values field in the object
func (s StringTable) String() string {
    lines := make([]string, 0)
    for _, row := range s.values {
        lines = append(lines, strings.Join(row, ","))
    }
    return strings.Join(lines, "\n")
}

// this non-attached function can also access private fields since they are in the same package
func ExtractValues(s StringTable) [][]string {
    return s.values
}

Now this code still works and gives the same result as before, but we now have a little more control over what we expose or not outside of the stringtable package.

Pillars of OOP: Inheritance and Polymorphism

This is where it gets tricker with Go.

Go doesn’t have the same familiar model as traditional programming languages. Instead of inheriting, you embed objects which in many cases feels similar.

If you’re anything like me, this would give you false confidence in understanding how Go works, which is really frustrating when it does something completely different! 😕

I’d suggest always testing your assumptions with simple code first to get a sense for what works and what doesn’t util you feel confident in a new language.

Embedding is actually quite fun to use, let’s go through an example and move our formatting logic into a separate type:

main.go:

package main

import "fmt"
import "example/stringtable"

func main() {
    // initialize the object
    table := stringtable.NewStringTable([][]string{
        []string{ "A", "B", "C" },
        []string{ "1", "2", "3" },
    })

    // go down the embedding graph (breadth-first) and call the first found GetFormat()
    // if multiple found at the same embedding depth, throws a compile error
    fmt.Println(table.GetFormat())

    // String() is called implicitly
    fmt.Println(table)
}

stringtable/table.go:

package stringtable

import "strings"

// let's decouple formatting logic into a separate type
type CSVFormatter struct {}

// this type doesn't own the values, but knows how to format them
func (f *CSVFormatter) FormatTable(values [][]string) string {
    lines := make([]string, 0)
    for _, row := range values {
        lines = append(lines, strings.Join(row, ","))
    }
    return strings.Join(lines, "\n")
}

// this method call could be "proxied" from the embedding "parent"
func (f *CSVFormatter) GetFormat() string {
    return "CSV"
}

type StringTable struct {
    values [][]string
    // embedded type
    // no polymorphism here, it's just this type
    // you cannot "assign" other types that embed CSVFormatter and "act" like it here
    CSVFormatter
}

func NewStringTable(values [][]string) StringTable {
    return StringTable{ values: values }
}

func (s StringTable) String() string {
    // another example of "proxy" call into the embedded type
    // this calls FormatTable on the CSVFormatter
    return s.FormatTable(s.values)
}

Polymorphism in Go

Finally, polymorphism feels a little different as well.

Because there’s effectively no subtyping, there’s no way we can treat an object as its base (parent) class and not care about the actual implementation.

Except… there is.

Instead of polymorphism through inheritance, we have Go interfaces`. Various types can implement aninterface`` and still be treated the same in the top level logic.

How do you treat this image? 😉

An image that is both a duck and a rabbit depending on your viewpoint

Let’s now see how we can extend our string table type to support different formats in a uniform way.

Our main.go will stay the same, changes will only be in the stringtablepackage.

stringtable/table.go:

package stringtable

import "strings"

// our existing CSV formatter
type CSVFormatter struct {}

func (f CSVFormatter) FormatTable(values [][]string) string {
    lines := make([]string, 0)
    for _, row := range values {
        lines = append(lines, strings.Join(row, ","))
    }
    return strings.Join(lines, "\n")
}

// fancy HTML formatter, now we'll be able to put our data on the Web! :)
type HTMLFormatter struct {}

func (f HTMLFormatter) FormatTable(values [][]string) string {
    lines := make([]string, 0)
    for _, row := range values {
        vals := make([]string, 0)
        for _, val := range row {
            vals = append(vals, "<td>" + val + "</td>")
        }
        lines = append(lines, "<tr>" + strings.Join(vals, "") + "</tr>")
    }
    return "<table>" + strings.Join(lines, "\n") + "</table>"
}

// common interface for the formatters above
// interfaces are implemented implicitly in Go
// there's no need to declare that a type "implements"
type Formatter interface {
    FormatTable([][]string) string
}

type StringTable struct {
    values [][]string
    // a formatter interface could be embedded as well
    Formatter
}

// retain old constructor for compatibility
func NewStringTable(values [][]string) StringTable {
    return NewStringTableWithFormat(values, CSVFormatter {})
}

// new constructor that accepts a custom formatter!
func NewStringTableWithFormat(values [][]string, formatter Formatter) StringTable {
    return StringTable{ values, formatter }
}

func (s StringTable) String() string {
    return s.FormatTable(s.values)
}

What we have learnt

I hope you had fun reading through this OOP refactoring! Throughout all of the changes in this post, the program output that we got in the first code snippet remained the same. In the end however we got to a point when we have well structured, modular and easily maintainable and extendable OOP code.

We have looked at:

  1. Difference between Go types and traditional classes
  2. Different approach to encapsulation in Go with package-level visibility and access
  3. How we can write well-structured code with embedding instead of inheritance and a little different polymorphism

If you like this material, bookmark my blog :)

Thank you for reading! You’ve been awesome!