Interfaces are my favorite feature in Go. An interface type represents a set of methods. Unlike most other languages, you don’t have to explicitly declare that a type implements an interface. A struct S implements the interface I implicitly if S defines the methods that I requires.

Writing good interfaces is difficult. It’s easy to pollute the “API” of a package by exposing broad or unnecessary interfaces. In this article, we’ll explain the reasoning behind existing guidelines for interfaces and supplement them with examples from the standard library.

“The bigger the interface, the weaker the abstraction”

It’s unlikely that you’ll be able to find multiple types that can implement a large interface. For that reason, ”interfaces with only one or two methods are common in Go code”. Instead of declaring large public interfaces, consider depending on or returning an explicit type.

The io.Reader and io.Writer interfaces are the usual examples for powerful interfaces.

type Reader interface {
	Read(p []byte) (n int, err error)
}

After grepping the std lib, I found 81 structs across 30 packages that implement an io.Reader, and 99 methods or functions that consume it across 39 packages.

“Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values”

By defining the interface in the package that actually uses it, we can let the client define the abstraction rather than the provider dictating the abstraction to all of its clients.

An example is the io.Copy function. It accepts both the Writer and Reader interfaces as arguments defined in the same package.

func Copy(dst Writer, src Reader) (written int64, err error)

Another example from a different package is thecolor.Color interface. The Index method of the color.Palette type depends on it, allowing it to accept any struct that implements the Color interface.

func (p Palette) Index(c Color) int

“If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself”

The previous guideline from the CodeReviewComments mentions that:

The implementing package should return concrete (usually pointer or struct) types: that way, new methods can be added to implementations without requiring extensive refactoring.

Complementing it with the statement from EffectiveGo, we can see the full picture where it’s okay to define an interface in the producer package:

If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself.”

An example is the rand.Source interface which is returned by rand.NewSource. The underlying struct rngSource within the constructor only exports the methods needed for the Source and Source64 interfaces so the type itself is not exposed.

The rand package has two other types that implement the interface: lockedSource, and Rand (the latter is exposed because it has other public methods).

What’s the benefit of returning an interface over a concrete type anyway?

Returning an interface allows you to have functions that can return multiple concrete types. For example, the aes.NewCipher constructor returns a cipher.Block interface. If you look within the constructor, you can see that two different structs are returned.

func newCipher(key []byte) (cipher.Block, error) {
  ...
  c := aesCipherAsm{aesCipher{make([]uint32, n), make([]uint32, n)}}
  ...
  if supportsAES && supportsGFMUL {
    // Returned type is aesCipherGCM.
    return &aesCipherGCM{c}, nil
  }
  // Returned type is aesCipherAsm.
  return &c, nil
}

Note that in the previous example, the interface was defined in the producer package rand. However in this example, the returned type is defined in a different package cipher.

In my short experience …

… this pattern is a lot more difficult to execute than the previous ones. In the early phases of development the needs of a client package evolves quickly. Hence, it results in modifying the producer package. If the returned type is an interface, it slowly becomes too big to the point where returning a concrete type would make more sense.

My hypothesis for the mechanics of this pattern is:

  1. The interface returned needs to be small so that there can be multiple implementations.
  2. Hold off on returning an interface until you have multiple types in your package implementing only the interface. Multiple types with the same behavior signature gives confidence that you’ve the right abstraction.

Consider creating a separate interfaces-only package for namespacing and standardization

This is not an official guideline from the Go team, it’s just an observation as having packages that contain only interfaces is a common pattern in the standard library.

An example is the hash.Hash interface that’s implemented by the packages under the subdirectories of hash/ such as hash/crc32 and hash/adler32. The hash package only exposes interfaces.

package hash

type Hash interface {
  ...
}

type Hash32 interface {
	Hash
	Sum32() uint32
}

type Hash64 interface {
	Hash
	Sum64() uint64
}

I suspect the benefits of moving interfaces to a separate package instead of exposing them in the subdirectories are two-fold:

  1. A better namespace for the interfaces. hash.Hash is easier to understand than adler32.Hash.
  2. Standardizing how to implement a functionality. A separate package with only interfaces hints that hash functions should have the methods required by the hash.Hash interface.

Another package with only interfaces is encoding.

package encoding

type BinaryMarshaler interface {
    MarshalBinary() (data []byte, err error)
}

type BinaryUnmarshaler interface {
    UnmarshalBinary(data []byte) error
}

type TextMarshaler interface {
    MarshalText() (text []byte, err error)
}

type TextUnmarshaler interface {
    UnmarshalText(text []byte) error
}

There are many structs in the std lib that implement the encoding interfaces. However, unlike hash which is consumed by the packages under crypto/, there are no functions in the std lib that accept or return an encoding interface.

So why is it exposed?

I believe it’s because they want to hint to developers a standard method signature for (un)marshalling a binary into an object. Existing packages that test if a value implements the encoding.BinaryMarshaler interface won’t need to change their implementation if a new struct implements the interface.

if m, ok := v.(encoding.BinaryMarshaler); ok {
    return m.MarshalBinary()
}

It’s worth noting that this pattern is not followed with the Resetter interface in compress/zlib and compress/flate packages as it’s duplicated in both packages. However, this appears to be a point of discussion even with Go maintainers (see CR comment#27).

Finally, private interfaces don’t have to deal with these considerations as they’re not exposed.

We can have larger interfaces such as gobType from the encoding/gob package without worrying about its contents. Interfaces can be duplicated across packages, such as the timeout interface that exists in both the os and net packages, without thinking about placing them in a separate location.

Takeaways

Defer, defer, and defer writing an interface to when you have a better understanding of the abstraction needed.

A good signal as a producer is when you have multiple types that implements the same method signatures. Then you can refactor and return an interface. As a consumer keep your interfaces tiny so that multiple types can implement it.

Thanks to Nick Fischer for reviewing early drafts of this post!