Interfaces in Go are named collections of function signatures that allow us to define an implicit structural behaviour. A single concrete structure can implement multiple interfaces, and this presents means of construction of polymorphic structures in Go. By using interfaces in our systems, we enable to provide different implementations without affecting the underlying logic of the actual code that makes use of the interface functions.
If we would define an interface for a database client which provides functions to allow us to interact with the database. The service that makes use of the database interface functions doesn’t need to know anything about the underlying logic that implements the actual calls to the database instance. Furthermore, this allows us to define and use various implementations of different databases, such as Postgres, Mongo, Cassandra, etc.
*“Be conservative with what you do, be liberal with what you accept.”* - Postel’s Law
By designing systems that make use of interfaces not only we can define an implicit structural behaviour but also to write black box tests for individual components by generating mocks based on interfaces using gomock.
On another hand, if we would have used a concrete structure instead of an interface in the service for the database, we would need to spin up an actual test database instance to test the service.
I’m not saying that it’s not possible, especially if it just involves spinning up a Docker container; however, this would become complicated instantly when our system would start to depend on multiple different components.
I have heard many various arguments around where the interfaces should be defined. Some prefer to define interfaces where you use them, meaning if you import a 3rd party package into your project, you would define the interface in your project. Though, I prefer to define interfaces beforehand in a common package within the project that provides implementations for those interfaces. There are a few reasons why I prefer this:
- By defining interfaces beforehand allows me to think more about what I’m trying to achieve and how everything would fit together from the project’s architectural point of view.
- I don’t need to think about implementation details, and I can use my predefined interfaces as part of my tests to generate mocks.
- Removing the necessity for anyone who uses my code to define the interface afterwards I have already provided the implementation.
Does it make sense to construct an interface after you have provided the implementation? What if I decide to change one of the functions, you would need to update the interface so that it complies with my code changes. Changing your code in this way is a bad practice, and you should be avoiding to introduce breaking changes. Instead of this, it’s possible to provide deprecation notice by adding
Deprecated: <describe why your interface is being deprecated> to the interface comment. It’s a great way of how I can notify the interface users that the interface provided as part of my project will no longer be supported.
I see an interface as a blueprint which encompasses a set of guidelines which to follow without dictating how something should be implemented. By providing interfaces beforehand, I’m also letting anyone who uses it to know that it will follow an implicit structural behaviour. I firmly believe that those who provide implementations should be responsible for providing the interfaces.