Go Patterns: Object-Oriented Programming

by about Go, Software Engineering in Technology

On the surface, Go doesn’t look like an object-oriented language. However, if we look deeper, OOP is not only possible, but an effective way to organize code.

Object-oriented programming is most often implemented using classes. (There are other ways, for example prototype-based OOP.)

Typically, a class-based OOP language has at least the following components in a class:

  1. A data structure.
  2. A function to initialize the data structure (a constructor).
  3. Various functions that work on the data structure (methods).

Go offers all of these, but you may find that the other OOP niceties, such as visibility keywords, are missing. More about that later.

The data structure ▲ Back to top

Let’s take a simple example: a blog post. The data structure would look like this in Go:

type BlogPost struct {
    Title string
    Author string
    Text string
}

We could add more fields, but you get the idea. In Java the blog post would look similar:

class BlogPost {
    public String title;
    public String author;
    public String text;
}

The constructor ▲ Back to top

The next part of the puzzle is the constructor. In Java we would create the constructor as follows:

class BlogPost {
    public String title;
    public String author;
    public String text;

    public BlogPost(
        String title,
        String author,
        String text
    ) {
        this.title  = title;
        this.author = author;
        this.text   = text;
    }
}

Here we are initializing the blog post with the specified parameters. In Go on the other hand, we would create a separate function that creates a BlogPost structure and returns a pointer to it.

func NewBlogPost(
    title  string,
    author string,
    text   string,
) *BlogPost {
    return &BlogPost{
        Title:  title,
        Author: author,
        Text:   text,
    }
}

In other words, the constructor is just a convenience method in Go to initialize the data structure. Typically, the constructor would contain additional code to validate the input and also return an error if the title is empty, for example.

This is a big difference between Java and Go: the use of the constructor is not enforced in Go. You circumvent it and create the BlogPost structure without a constructor. We will talk more about this in the encapsulation section.

A primer on receivers ▲ Back to top

In the next segment we’ll use receivers. If you know what they are feel free to skip this section.

Receivers are a way to pass context to a function. You can create a data type like this:

type MyDataType struct {
    Title string
}

You can then use this struct as a receiver:

func (m *MyData) MyFunc() {
    m.Title = "Hello world!"
}

You can now use the data structure as follows:

myData := &MyData{}
myData.MyFunc()
print(myData.Title)

You can also use the receiver without a pointer, but in this case the function will not be able to change data on the data structure:

func (m MyData) MyFunc() {
    // This won't work.
    m.Title = "Hello world!"
}

Methods ▲ Back to top

Now let’s take a look at the methods. In Java, you can reference the class data structure with the this keyword. In Go on the other hand, you get a bit more flexibility by using receivers. We can use receivers to give the function the this variable as context.

func (this *BlogPost) UpdateTitle(title string) {
    this.Title = title;
}
Naming the receiver variable this is not good Go style. Instead, a common way to reference this variable would be: b *BlogPost.
Receivers are just syntactic sugar.
Receivers are nothing magical. They just move the first parameter of the function to a separate place.

Encapsulation ▲ Back to top

One feature of object orientation that is held in high regard is encapsulation. In class-based OOP this is usually reached by not making the data structure accessible from the outside, only via methods. While this may sound inconvenient, it helps to enforce the business rules.

For example, in Go nothing prevents you from creating a BlogPost copy without providing a title, author, or text. If you can only create a BlogPost only via the constructor, on the other hand, it can enforce the rules on how long or short the title has to be, etc.

One of the main problems in creating encapsulation in Go is its rather simplistic scoping mechanism:

  1. If something is written with a capital first letter it is exported to other packages, so any package can access / call that thing.
  2. If something is written with a lower case first letter it can only be accessed within the current package.

There is no mechanism in Go to restrict visibility within a package, even though it would sometimes be very useful. This is especially true when working in a larger team and not everyone is aware of all the business rules that govern the application.

A good way to bring a bit more discipline into our code is the use of interfaces. Sticking with our example above, we can create a lower cased blogPost struct and then a capital letter BlogPost interface. Since the constructor declares the interface as a return type it makes it harder for callers to mistakenly access an internal functionality of blogPost. In our example it prevents someone from accidentally changing the posts title.

type BlogPost interface {
    GetTitle() string
    GetAuthor() string
    GetText() string
}

func NewBlogPost(
    title string,
    author string,
    text string
) BlogPost {
    return &blogPost{
        title: title,
        author: author,
        text: text,
    }
}

type blogPost struct {
    title string
    author string
    text string
}

func main() {
    post := NewBlogPost(
       "Go Patterns: OOP",
       "Janos",
       "..."
    )
    // This will result in a compile-time error
    post.title = "Go Patterns - OOP"
}

A caller would have to explicitly do a type assertion to *blogPost to be able to overwrite the title. This usually gets people thinking and asking questions within the team on why this behavior is in place.

Inheritance ▲ Back to top

Another important corner stone of OOP is inheritance. It allows you to inherit behaviors (methods, data structures) from a parent class.

Inheritance is overused
It should be noted that in my experience 90% of cases where inheritance is used it is uncalled for. This is described in more detail in my post titled What people misunderstand about OOP.

Inheritance can be implemented fairly easily in Go. For our example we are using a bank account or banking product. Nowadays (in the EU) each bank account, loan, etc. has a unique IBAN, but they behave much differently. Let’s model that in Go:

type BankingProduct struct {
    iban string
}

func (b *BankingProduct) GetIBAN() string {
    return iban
}

Once we have our BankingProduct we can now create the BankAccount and Loan “subclasses”:

type BankAccount struct {
    BankingProduct

    // Don't use float for monetary values.
    // Instead, use or implement a decimal type.
    balance Decimal
}

type Loan struct {
    BankingProduct

    outstandingBalance Decimal
}

If we have done this we can call GetIBAN() on a copy of BankAccount or Loan. However, we can unfortunately not pass copy of BankAccount when BankingProduct is required:

func doSomethingWithProduct(product BankingProduct) {

}

func main() {
    account := &BankAccount{}
    doSomethingWithProduct(account)
}

As before, we can solve this problem by introducing an interface:

type BankingProduct interface {
    GetIBAN() string
}

type AbstractBankingProduct struct {
    iban string
}

func (a *AbstractBankingProduct) GetIBAN() string {
    return iban
}

If you are wondering, yes, this is painful. It’s one of the shortcomings of Go that OOP leads to a fair bit of boilerplate code.

Conclusion ▲ Back to top

This illustrates how OOP can be implemented in Go. It is not as simple to type as in other, OOP-focused languages, but it is definitely viable. It is worth noting that Go definitely has severe shortcomings when it comes to visibility and scoping too. If you are looking for a language that can house complex business logic for your ERP or billing system you may want to look elsewhere as Go brings tradeoffs with it.