Dependency Isolation in Go
If you’ve never written any Go code, grasping the mechanics of its interfaces could be burdensome at first. The fact that you don’t need to explicitly declare that a struct
implements a certain interface will make newcomers a little more than confused. But let me tell you this; it’s a tremendously powerful feature.
But first a little bit of history…
If you’re a serious programmer you must be familiar with SOLID principles. The 5 principle which Uncle Bob coined and published as a guide for us to write good software. The fifth of those guidelines is the Dependency Inversion Principle
which states:
High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
But that’s for Object Oriented Programming right?! Is Go even an object oriented language? No it’s not. And you should never think of it as one.
But even though Go is not an OO language, many of our previous practices holds true here too. Specially the Dependency Inversion Principle
. So what’s Go’s interfaces have to do anything with it?
Let’s continue with an example. Imagine that you’re building a package which needs to store some data, but it doesn’t not care about where or how this data is stored. Let’s call it foo
. Let’s also have Fooler
interface which will have a Save
func that another package will use.
As you can see the foo
struct conforms to the Fooler
interface. But where do we store the data? Let’s add an Storage
interface so that we can use it to store some data.
But wait…!
We have no idea what functions that Storage
interface is going to have. What if it names its Save
function something like Store
? What if it accepts different kinds of parameters in different order?
The cool thing here is, that we don’t really care about how or what methods that external persistence service will have. We define our own Storage
interface and as long as it -the external service- conforms to our standards we can use it. Without any imports for external interfaces. As a matter of fact that other package doesn’t need to import anything from us too. That is called Duck Typing
[1]. If it walks like duck and it quacks like a duck, then it is a duck.
So let’s define our interface like this.
Now that we have our Storage
we can refactor our foo
struct.
We’re not done yet. We need a way to pass in the Storage
instance to the foo
package. It is conventional in Go to have an exported function which instantiates things for other packages to use. This is how it goes…
So now let’s create a sample storage package to see how it goes.
As you can see there is no import
anywhere in both our packages. The only entity which needs to be aware of our individual interfaces is the higher level package which will pass the Datastore
to the NewFooler
function. Here is the main package that will glue these two packages together.
main
is the only package which needs to be aware of those 2 components. You now may begin to understand how powerful and flexible this feature is in creating highly decoupled software components.
Go is a simple enough language to learn but it is difficult master. It has many intricate features such as this one that only practice can make a perfect gopher.
Footnotes:
[1] Technically in Go Duck Typing
is called Structural Typing
since it is done at compile time whilst Duck Typing
is done at run time.