Interfaces is a very important feature in Go. It is a key to implement polymorphism and dependency injection in Go. In spite of its importance, there are some serious arguments about the best practice of Go interfaces, which may lead to another little-endian v.s. big-endian war in the future. The purpose of this blog to is to discuss these arguments in a peaceful way and find a way to avoid this potential war (if possible).
Introduction to Go Interfaces
The following shows an example of Go interface:
From the above example, you can see that:
- A Go interface defines one or more methods.
- Unlike other languages, you don’t have to explicitly declare that a type implements an interface. A struct
Sis a thing which is defined by an interface
Ias long as the struct
Simplements all the methods defined by the interface
I. In this example,
FlyBehaviouras they all implement the methods defined in the interface
Best Practice (a.k.a. Arguments)
The concept and usage of Go interfaces is simple. However, there are some arguments regarding its best practice. Now let us discuss those arguments and find some agreement regarding the best practice of Go interfaces.
“The smaller the interface，the stronger the abstraction”
There is no argument about this best practice as this is basically another expression of Interface Segregation Principle.
The following shows an example that follows this principle, in which the interface
user.Manager is split into multiple smaller interfaces.
"Accept interfaces but return structs”
We all agree on accepting interfaces other than concrete implementations as this is the key to follow SOLID principles (e.g. Dependency Inversion Principle) and realize some design patterns (e.g. Decorator Pattern) in Go. However, there is no solid answer to returning interfaces (abstraction) or structs (implementation).
The benefit of returning structs is to give consumers the freedom to define interfaces on their side. Take the above interface
user.ManagerInterface as an example, consumers can define this interface based on the struct
user.Manager (the implementation of user manager) and their need in their own packages.
The benefit of returning interfaces is to free consumers from defining interfaces on their own, as long as the returning interfaces provide strong abstraction. Moreover, some design patterns, such as Factory Method Pattern, require to return interfaces other than concrete implementation. For example, the method aes.newCipher returns the interface cipher.Block with different structs in different circumstances:
Normally, the smaller the interface，the stronger the abstraction. The stronger the abstraction, the more acceptable it is to return interfaces.
“Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values”
“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. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface.”
So where should we define interfaces? Consumer side or producer side? I think this depends on whether the interface that you define has good abstraction or not. If an interface provides very strong abstraction, then it makes sense to put it in an individual shared package. The interface
io.Reader in the package io is a perfect example of interfaces with strong abstraction. It only has one method
Read and everyone agrees what this method should look like. Therefore, everyone is fine with putting the interface
Reader in the package
io, which is a producer that provides implementations of this interface.
The opposite example of putting Go interfaces on the consumer side is Go client of Github. It is apparently not a good idea to define a
github.Client interface as a Github client needs to provide a lot of methods, which leads to weak abstraction for making a
github.Client interface. In this case, it makes more sense for consumers to define their own
github.Client interfaces, which may vary from consumer to consumer and may only have very few methods.
“It can be useful to define a default abstraction on the producer side”
It is easy to determine where to put an interface when the interface provides either very strong or very week abstraction. But we are not always that lucky in reality. Take the above interface
user.ManagerInterface as an example, it makes sense to split it into multiple smaller interfaces. But do these interfaces provide strong abstraction so that it is acceptable to define them at the producer side (the package
user)? Or should only this producer provide the concrete implementation of user manager?
Suppose the package
user is an internal package for a project and it has five consumers (five packages are using this package). The interface
user.ManagerInterface needs to be defined five times in these packages for programming to interfaces, if there is no default interface
user.ManagerInterface and it is very likely these packages may define the same interface
user.ManagerInterface. Therefore, it can be very useful for the package
user (producer) to provide a default interface and its mocks in this case. Here is the pseudo-code:
From the above example, you can see that:
- The implementation
user.Manageris exposed and returned, which gives consumers the freedom to define the abstraction of
user.ManagerInterfacebase on this implementation.
- The package
userprovides a default abstraction of
user.Managerand its mocks through the interface
user.ManagerInterfaceand the struct
user.ManagerMock, which frees some consumers from defining the abstraction of
user.Managerand its mocks.
- This pattern is a trade-off of the argument about where to define interfaces. That is, provide default abstraction on the producer side but also give consumers the freedom to define their own abstraction. And most importantly, let consumers decide what they want to use.
This blog talks about several aspects of interfaces, including the size of interfaces, where to define interfaces, return structs or interfaces, and the benefit of providing a default abstraction for an implementation. Like uncertainty of life, there is no solid answer to these arguments/questions. But I do hope this blog help you clean up some puzzle about Golang interfaces. And most importantly, may the world piece forever.