A Note on Using Go Interfaces Well

2018-03-30

How to use interfaces in Go has been a perennial topic for people writing about the language. In spite of the continued treatment, there remains some misunderstanding, likely caused by people's experience with interfaces in other languages.

An interface makes a minimal demand on a caller

Rather than thinking of an interface first and then an implementation, one should use an interface to be generous with what one accepts. This is a restatement of the robustness principle.

Or, in other words, accept interfaces, return structs.

If a function signature accepts an interface, then callers have the option to pass in any concrete type, just as long as it implements that interface.

One implication of this idea is that interfaces should be declared close to where they are used, not where the interface's implementations may be.

This may be the hardest idea to grasp for people coming from other languages. There is no implements keyword. It is the compiler which ensures a particular type implements an interface.

A large interface is less useful than a small one

As Rob Pike has said, the bigger the interface, the weaker the abstraction.

The more methods included in an interface, the more burden is placed on the implementation. In addition, the more methods there are, the more specific the interface is to a particular usage. A larger interface results in implementations often having to implement methods that are unrelated to their particular purpose.

There is no mistake that the most famous interfaces, io.Reader and io.Writer, have only one method each.

An interface ensures implementations honor a contract

Another valid usage of interfaces is to ensure any number of types all implement the same interface. At first glance, this may sound like a contradiction of the idea that interfaces make a minor demand on a caller, but in fact it is a separate usage, equally worth knowing and using when appropriate.

Consider the needs of a store or repository which may be backed by any number of concrete implementations. In this case, an interface provides a means to ensure each implementation (e.g., the redis store, the postgres store, the mongo store, etc.) satisfies the needs of what a store must do. For example, see here.

An empty interface says nothing

Those familiar with the Go proverbs will recognize the statement "interface{} says nothing." Within a well designed system, types convey information useful to different parts of a program. It is only at the edges of a program, where for example one must consume data of an unknown format from a remote server, where things like the empty interface are a necessity.

Using interface{} is usually an indication of over-generalizing one's code in a game to save on the number of characters typed. There are exceptions worth considering, but generally, over using the empty interface is an opportunity to think a little more about one's design.

Further Reading