Enable javascript in your browser for better experience. Need to know to enable it? Go here.

Go interfaces

Mistakes to avoid when coming from an Object-Oriented language

Go is simple, but that doesn’t mean it’s easy to learn. While its simplicity comes from its familiar syntax, small amount of keywords and opinionated design, some of these design decisions paradoxically have  significant consequences and make the language hard to master. 

 

One such decision is the paradigm used to design the language. Go is not an Object-Oriented (OO) language! It does a good job looking like one, but it’s not. Interfaces are a perfect example of an OO concept used in Go. Although they are very similar to interfaces in other OO languages,  to master them it’s essential to understand some key differences that are inherent to Go. In this article we’ll take a closer look at some of those differences so we can ultimately write better Go code. We will define one rule for working with interfaces. 

 

The first thing to do — and probably the hardest — is to forget about objects, classes and inheritance. Those concepts do not exist in Go!

 

Interfaces are simple

 

The official Go documentation offers some details on how to use interfaces (e.g. A Tour Of Go, Effective Go). Although useful, they don’t really scratch the surface. They tell us the basic syntax, but they don’t offer meaningful guidance on design, tradeoffs or pitfalls.

 

There is lots of additional information available on the wider web — much of this provides useful insights into best practices, yet all too often they lack detail about why this practice is best (e.g. Using interfaces the right way, this question on Reddit). Interestingly, Google's own documentation has most of the relevant information available. However, this is typically buried deep in style guides (e.g. Code Review Comments, Go Style Decision).

 

So, in other words, information is everywhere, but it is hard to find relevant information. This can be particularly frustrating because there is an awful lot of implicit knowledge in Go.

 

Before we jump into action, let’s first define a producer as a piece of code that provides an API and a consumer as a piece of code that uses said API. Producer and consumer could be in the same package, different packages or modules, or even different libraries. Those two concepts are central to understanding how the Go paradigm differs from OO languages.

 

With those definitions in mind, we can now state this rule: interfaces should be defined on the consumer side. We will look at an example of OO-thinking applied to Go, highlighting the issues with it. We will then propose a more idiomatic approach and see the benefits.

But interfaces aren’t easy!

 

Let's say we are experienced Java developers. We are now assigned to a Go project and we are growing our knowledge of the language.

 

We need to implement a functionality to store messages. Coming from our OO background, we analyze our requirements and come up with an interface with the methods required by the various consumers of our API. We then define a concrete type that represents an in-memory store. This example is illustrated in the following snippet. Implementing the methods is left to the creativity of the reader!

// Producer

type Message struct{}

type MessageStore interface {
    Get(index int) (Message, error)
    Add(m Message) error
    AddAt(index int, m Message) error
    Remove(m Message) error
    RemoveAt(index int) error
    Clear() error
    Contains(m Message) bool

    Size() int
    Empty() bool

    First() (Message, error)
    Last() (Message, error)

    Sort() error
    SortWith(f func(l, r Message) bool) error
    Filter(f func(m Message) bool) (MessageStore, error)
    FilterInPlace(f func(m Message) bool) error
}

type InMemoryMessageStore struct {
    store []Message
}

From there, we can implement consumers that will use this interface. The following snippet illustrates one such consumer whose role is to print messages.

// Consumer

func PrintMessages(m MessageStore) {
    for i := 0; i < m.Size(); i++ {
        fmt.Println(m.Get(i))
    }
}

Do you see the problem?

 

The consumer only needs to access messages. It does not need to add, remove or sort them. But because it consumes the MessageStore interface, it knows all about it. This function is tightly coupled to implementation and design details proper to the producer.

 

MessageStore exports many methods. Some may not even be used. This is known in Go as interface pollution. We export a large, bloated interface that is hard to change. And we created one interface to be implemented by one single type.

 

Testing the consumer is difficult. We need to create a test double type that implements the interface. And if we want an actual mock, the test double code will become extremely verbose. There are tools to help us generate that code, but it will still be an awful lot of boilerplate. We could use the concrete type, but that comes with its own limitations. What if the store persists messages on the disk? Or store messages on a remote service?

 

Let's try to understand what went wrong. In an OO language, the concrete type has to explicitly implement the interface, so the interface has to be defined on the producer side. Things are a bit different in Go — here an interface is implemented implicitly. A type only needs to implement the methods of that interface. We can see that as some sort of structured duck-typing. The concrete type does not have to know about the interface. As a corollary this means the interface can be defined on the consumer side by what the consumer needs.

 

This is where Go differs from OO languages. Go does not have type hierarchies because Go does not focus on identity. In Java for example, an ArrayList is a List which is a Collection, while also being Iterable and Serializable, among other things. In Go, an interface simply defines a behavior. A writer writes, a reader reads. They might be implemented by the same entity, but they have distinct behaviors. Consumers are only interested in behaviors.



Embracing the Go philosophy

 

Let's rework our example with that in mind. Our consumer only needs to access messages. On the consumer side, we can create a simple interface that specifies this requirement, as illustrated in the following snippet. The method signatures of this interface happen to match the methods of the concrete type.

 

// Consumer

type MessageAccessor interface {
    Get(index int) (Message, error)
    Size() int
}

func PrintMessages(m MessageAccessor) {
    for i := 0; i < m.Size(); i++ {
        fmt.Println(m.Get(i))
    }
}

On the producer side, we remove the interface. The rest of the code is the same, the concrete type can define as many methods as needed. The following snippet shows the minimal code required on the producer side to work with our consumer.

// Producer

type Message struct{}

type InMemoryMessageStore struct {
    store []Message
}

func (m InMemoryMessageStore) Get(index int) (Message, error) {
    if index < m.Size() {
        return m.store[index], nil
    }

    return Message{}, errors.New("index out of bound")
}

func (m InMemoryMessageStore) Size() int {
    return len(m.store)
}

 

Why is this an improved solution?

 

  1. The API dependencies are kept to a minimum. The consumer expects a behavior specified by the interface and knows nothing else.

  2. The producer does not export unnecessary interfaces, thus there is no interface pollution.

  3. Testing the consumer is easier. Mocking or stubbing requires only two methods; it can be done inline while maintaining readability.

  4. Coupling between producer and consumer is minimized. If the producer changes, it is trivial to adapt the interface. In the most complex situations, we can use the adapter pattern to manage the differences.

 

However, looking at those points carefully, they can all be applied to any OO language. They are manifestations of SOLID principles. The key difference is what Go allows us to do. Because interfaces are implemented implicitly they can, and should, be implemented on the consumer side. This way, they can be broken down to their most simple form (it is common in Go to have interfaces with one single method, e.g. Stringer, io.Reader or io.Writer from the standard library). On the producer side, no need to overthink our design and try to foresee every potential use case.

 

The most important thing we had to do was take off our OO-thinking-cap!

 

An example from the real world

 

This contrived example illustrates the recommended way to work with interfaces. But what about the real world?

 

The websocket module referred to by the Go documentation produces a type Conn which represents the websocket connection, and several methods and functions to operate the websocket. It does not define any interface. A consumer of this module will have to define its own requirements through one or several interfaces.

When not to apply the rule

And what would be a rule without counter examples?

 

The io package from the standard library defines and exports several small interfaces such as Reader and Writer. It does so because it also exports several functions using those interfaces (e.g. Copy(Writer, Reader)). It reverses the flow by exporting interfaces for the consumer to implement, so the consumer can use the exported functions. In this context, the consumer of the io package is actually interested in the functions — the interfaces are just a means to an end.

 

Closing notes

 

In this article we have seen the shortcomings of writing Go code with an OO mindset. It bears repeating that Go is not an OO language! Coming from OO, we need to forget what we know and learn to embrace the Go philosophy. An interface should be written on the consumer side. This allows for a simpler design and fewer dependencies, resulting in cleaner code. Writing an interface on the producer side is still an option, but we need to weigh the pros and cons before choosing this approach.

Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.

Keep up to date with our latest insights