When I learned Golang, coming from dynamically typed languages, Interfaces were something new to me. I struggled a little to grasp them I recall.
Years later I know why: because it wasn’t clear to me why we needed them, or, put another way, what problems they solved for us.
I had to encounter the problems for myself.
So, the “why” and the “what” is where we are going to start and finish in this tutorial. By the end you should know why we use Interfaces in preference to types like Structs, and what problems they enable us to solve in our application code.
It’s also my hope that by the end of this tutorial, you’ll know Go interface syntax fairly well; well enough to start refactoring and implementing them for yourself.
This week’s tutorial takes the form of a fictional anecdote; a fable if you like. We set up a scenario, show how things get complicated if we code with concrete types like Structs, and then demonstrate how we can solve the problems we’ve created by refactoring to abstract types, which we call Interfaces.
At the end of the scenario, our Boss is so happy with what we’ve done, we all get a 200% pay rise.
Do we heck. This is a fable remember!
Let’s get started.
ACME MaC Factory
Imagine we run a factory, and we’re doing the latest MaC (Manufacturing as Code). We can make anything in our factory, but so far we just make cars.
Check out the BuildCar function below. The implementation of BuildCar is unimportant - we call it to build a car, it returns a type of Car, itself a Struct. It represents our car - every single aspect of it.
type Car struct {...}
func BuildCar() Car {...}
We get asked if we can manufacture boats - and we can - so we write the code and start production.
type Boat struct {...}
func BuildBoat() Boat {...}
Same as before, we call BuildBoat, but we receive a representation of a Boat in return. We make a lot of boats. Our Boss is loving it: this is cutting-edge manufacturing and he’s making a fortune!
Word gets around. People ask us to manufacture caravans, motorcycles, and washing machines, of all things!
Boss says yes. Of course he does! Our Software Engineer, says “no problem” and he adds more code.
type Caravan struct {...}
func BuildCaravan() Caravan {...}
type Motorcycle() {...}
func BuildMotorcyle() Motorcycle {...}
type WashingMachine struct {...}
func BuildWashingMachine() WashingMachine {...}
Another Engineer points out that we are duplicating a lot of the same code in these functions, and as a result, it’s taking longer to maintain.
That’s a valid point agrees our Boss, “but park that thought for now”, he says.
We carry on making stuff, but, as we make these products in greater and greater numbers we notice we’re getting *a lot* more complaints.
The Boss tells us that the root cause is that *we* are not managing quality properly, so it’s agreed we need to put each of our products through a Test & QA process before we call them done.
No problem, we ask our Engineers to add these processes as new functions, and we’ll simply inject each product as a dependency to the new Test and QA functions. Should be simple.
Our Engineers blink back at us. They look at each other - and shudder.
Why? Because we are using Go, and Go is statically typed. Which means, we need to specify the type of the product which we pass to these new functions, so, as a result of static typing, we can’t just have two generic functions to handle all of our products.
Instead, it would seem we need a Test and QA function for each of our products!
That’s ten more functions we need to write and maintain, even though the code is probably substantially similar between them - although not completely so.
The Engineers make a start, this codebase is becoming high maintenance they mutter.
Then one Engineer remembers that Go has Interfaces.
He recalls that Interfaces specify behaviour and that they do this by defining methods which represent these behaviours.
He also recalls that in Go, Interfaces are not explicitly implemented, but implicitly. A type will implement an Interface if it exhibits the behaviours of the interface, automatically.
His train of thought takes him as far as something known as the empty interface, an interface that specifies no behaviours (no methods).
All types implement this interface implicitly since in Go they all have “at least” no methods.
Reinvigorated, our Engineers delete eight of the new functions, and refactor the Process and QA functions down to just two, with these signatures.
func Test(product interface{}) interface{} {...}
func QA(product interface{}) interface{} {...}
Now we can inject any product into these functions and return any product from them because we’re using the empty interface, which as we noted, represents any (and all) types in Go.
Job done, our trusty Engineers have future-proofed the MaC codebase. The company can make anything, adding new products as-and-when, and the codebase will handle it, one Engineer informs the Boss.
The Boss is pleased.
Another Engineer performs a peer review on the codebase. He agrees, this approach is better than it was, but he has an uneasy feeling which he can’t quite pin down.
Then the penny drops.
Though much of the code in these functions is generic so can apply to all products, some of it isn’t, so cannot. For example, you can’t Test and QA a Car in exactly the same way as you as do a Caravan, and certainly not a WashingMachine.
To their dismay, the Engineers realise that for the Test and QA functions to work properly, the code in these functions still needs to understand the product or type it is working with in order to process it correctly.
One recalls how Go has type assertion and implements a type switch in each of the Test and QA functions, essentially a switch statement that lets him handle different types with different logic.
It looks something like this:
switch p := product.(type) {
case Car, Motorcycle:
...
case Boat:
...
case Caravan:
...
case WashingMachine:
...
default:
...
}
Again the ellipsis represents code logic we don’t need to care about at this point, other than to say it is different logic for most products.
We’re saying that we can handle Car and Motorcycle using the same code, but Boat, Caravan and WashingMachine must be tested and quality assured with different and individual logic.
They implement all the code. It’s better than it was the Engineers agree.
But is this really better?
If we’re objective we can see that we’ve moved maintenance of duplicated code from outside (multiple product specific Test and QA functions) to inside the Test and QA functions.
It may be slightly better than maintaining separate functions for each product, but every time we add a new product to our range we’ll need to maintain these type switches.
One could also argue the code is now less clear - we need to drill deeper to understand the program flow and to change it in the future.
Note also how we’ve given up on type safety. These functions will accept any type. What if we get something we don’t expect at runtime? In the code above default will handle it, but not knowing the type can lead to unexpected behaviour; will it handle it as we expect?
And, there’s another problem. We’re also returning a type of empty interface. Now any code that follows the Test and QA functions cannot depend on concrete types either. So the same type switch assertion logic has to be applied again and again to establish how to treat the type we are working with.
The Engineers manage to change all the code but feel like they’re missing something since there’s so much duplicated code!
Go is rubbish, they decide. One goes as far as to start learning PHP.
The next day our Boss, never one to stop innovating, says we need to package our products better, and also polish them before delivery. He also wants each product personalised with the name of the customer, engraved on a badge, and attached somewhere to each product.
Our deflated Engineers, revisit the codebase yet again. It quickly dawns on them that they will have to add three new functions, all similar to the Test and QA functions written previously.
func Package(product interface{}) interface{} {...}
func Polish(product interface{}) interface{} {...}
func Personalise(product interface{}) interface{} {...}
But, now they must maintain the type switch logic in five places, not two!
Worse, every time the company decide to make a new product the team must provide code for that product in five different functions.
Let’s hope the Boss has a week off…
Of course, the Boss doesn’t have a week off. Instead, he wins a contract to manufacture Trailers.
Our Engineers are beside themselves as they contemplate the ramifications of yet another product on the codebase; the type switches in the five functions and throughout all the subsequent code.
It’s too much. One Engineer picks up a book on Node.js.
The last Engineer is perplexed. Everyone says Go is a great programming language, this is why she started to learn Go.
Something isn’t right she thinks, it shouldn’t be this hard.
She takes a sip of coffee, sits down to grapple with the codebase yet again, and adds the Trailer struct and BuildTrailer function following the conventions already used.
type Trailer struct {...}
func BuildTrailer() Trailer {...}
Immediately, she spots the problem. Potentially it is our reliance on the empty interface that is causing all our problems. We need named interfaces, not empty interfaces.
Our Engineer now recognises the two primary requirements for this codebase.
We need to be able to handle new products as-and-when they arise because the Boss will make anything evidently, and, we need to be able to deal with different operations or processes for each product, but also some similar/same operations.
For example, it’s conceivable that the Test and QA processes for Caravan and Trailer could be similar in many respects, yes?
But equally, for example, it’s simply not appropriate to apply a personalisation plate to a Caravan dashboard, is it? It doesn’t have one.
She decides to refactor the code. She won’t be relying on empty interfaces, instead, she will create named interfaces, several of them in fact, each representing a different set of characteristic behaviours.
She is going to move the code away from using concrete types as dependencies, which gave rise to the need to use the empty interface, and instead use abstract types. Test and QA functions will be changed to accept only types with certain defined behaviours.
Let’s now put ourselves in her shoes, and see what this might mean for the codebase. The immediate requirement is to be able to Test, QA, Package, Polish and Personalise all the products.
In Go, interfaces like structs are composable. Smaller interfaces can be combined together into new interfaces that represent sets of behaviours.
This invariably means smaller interfaces that specify fewer behaviours are preferable to larger interfaces that specify many behaviours. Because we have composition there’s really nothing to be gained in large interfaces.
We’re going to refactor the code to demonstrate, but only the Test and Personalise functions. To do more would mean we’d have a lot of code and may risk missing key points.
We’re also going to build this code up in layers, and over two examples, so we will have the opportunity to explain clearly what we are doing in each step.
Example 1
What Interfaces do we need?
To decide this we look at our product range, not in terms of concrete types, but in terms of their behaviours or characteristics.
We’ll start with an obvious interface that all products satisfy: we’ll call it Makeable. We add a Test method, which returns true or false, and a similar Personalise method. Notice how we do not provide any implementation detail. Our product types will provide this, on a type by type basis.
// all products are makeable, all need to
// be tested and personalised
type Makeable interface {
Test() bool
Personalise() bool
}
Implement the Interface
There are two ways to do this. We can add the interface methods to a concrete type to implement the interface. On larger interfaces, this is quite verbose, especially if you really only want your struct to use a small part of the interface behaviour.
The second approach employs a handy trick. We can embed the interface into the struct. In doing so, we implement all the methods of the embedded interface but as no-ops - methods that do nothing. Then we can then implement or override just the methods we need.
For our example, we’ll go the first route and implement the methods on the struct types since there are only two.
// car
type Car struct {}
func (c Car) Test() bool {
fmt.Println("testing the car")
return true
}
func (c Car) Personalise() bool {
fmt.Println("personalising the car")
return true
}
// caravan
type Caravan struct {}
func (c Caravan) Test() bool {
fmt.Println("testing the caravan")
return true
}
func (c Caravan) Personalise() bool {
fmt.Println("personalising the caravan")
return true
}
// washing machine
type WashingMachine struct {}
func (wm WashingMachine) Test() bool {
fmt.Println("testing the washing machine")
return true
}
func (wm WashingMachine) Personalise() bool {
fmt.Println("personalising the washing machine")
return true
}
// trailer
type Trailer struct {}
func (t Trailer) Test() bool {
fmt.Println("testing the trailer")
return true
}
func (t Trailer) Personalise() bool {
fmt.Println("personalising the trailer")
return true
}
Refactor the Test and Personalise functions
We could leave the naming as-is, but, for the sake of clarity let’s just have one “finish” function.
The Finish function accepts any type that satisfies the Makeable interface type and returns the same. It calls the Test and Personalise methods the Makeable interface. Any product that satisfies the Makeable interface must have these methods.
func Finish(product Makeable) Makeable {
if ok := product.Test(); ok != true {
log.Fatalln("test failed")
}
if ok := product.Personalise(); ok != true {
log.Fatalln("personalisation failed")
}
return product
}
Process some products through the Finish function
We create a slice of type Makeable, the interface, and add one of each of our products in to slice using slice literals.
We’ll range through this slice and pass each product to the Finish function.
func main() {
products := []Makeable{
Car{},
Caravan{},
Trailer{},
WashingMachine{},
}
for _, v:= range products{
Finish(v)
}
fmt.Println("Production finishing complete")
}
Edit 18/10/20: I’m cheating a little here. I add the products as struct literals to this slice, the reason being that we avoided all the implementation logic on the factory functions earlier. The products slice isn’t a refactor, just a short-cut for our tutorial. Factory functions that create and return a type are the way to go. They allow us to ensure the right properties are set on each struct, whereas struct literals, like those we use above, allow us to get away with no, or at best incomplete configuration. I add this in response to a question I received on Twitter. Thought it worth clarifying.
The fact the Go compiler allows us to add the different concrete types to the slice shows that they each implement the Makeable interface. We would get compile-time errors if they did not.
You can find the entire code example on the Go Playground here: https://play.golang.org/p/dkJBNe9285t.
So far, so good
Can you see how we have better future-proofed this code? Instead of managing a type switch in each of the Test and Personalise functions, we simply call the appropriate method of the product type.
When we add another more product we only need to define the Test and Personalise methods on the product to have it satisfy the Makeable interface and be acceptable to our Finish function.
Unless we add more processes to the finish function we’ll never have to change this code again.
Example 2
Unfortunately, our Boss is back. He’s not happy that products with electrical connections still seem to have quality issues, he wants our MaC finishing process to include an electrical check for these products.
This presents us with a challenge. Not all products have electrical connections, only Caravan, Trailer and WashingMachine do. And they don’t have the same connection. The Caravan and Trailer have an electric hook-up, while WashingMachine has a mains wall-plug.
How should we handle this?
Well, we could add additional logic to the Test methods of the relevant products, but this feels a little opaque. It’s not a part of the test process, it’s a new QA process our boss wants. Adding the code there doesn’t sit right with us.
So, instead what we decide to do is create another Interface which we’ll name Pluggable. Pluggable will have a single CheckPlugConnection method.
Let’s now add this to our code example.
First, we create the new interface, and then we implement it on the appropriate product types by defining the CheckPlugConnection method. Notice how we don’t implement it on the Car struct.
package main
import (
"fmt"
"log"
)
// all products are makeable, all need to
// be tested and personalised
type Makeable interface {
Test() bool
Personalise() bool
}
// some products need to have their plug checked
type Pluggable interface {
CheckPlugConnection() bool
}
// car
type Car struct{}
func (c Car) Test() bool {
fmt.Println("testing the car")
return true
}
func (c Car) Personalise() bool {
fmt.Println("personalising the car")
return true
}
// caravan
type Caravan struct{}
func (c Caravan) Test() bool {
fmt.Println("testing the caravan")
return true
}
func (c Caravan) Personalise() bool {
fmt.Println("personalising the caravan")
return true
}
func (c Caravan) CheckPlugConnection() bool {
fmt.Println("checking caravan plug")
return true
}
// washing machine
type WashingMachine struct{}
func (wm WashingMachine) Test() bool {
fmt.Println("testing the washing machine")
return true
}
func (wm WashingMachine) Personalise() bool {
fmt.Println("personalising the washing machine")
return true
}
func (wm WashingMachine) CheckPlugConnection() bool {
fmt.Println("checking washing machine plug")
return true
}
// trailer
type Trailer struct{}
func (t Trailer) Test() bool {
fmt.Println("testing the trailer")
return true
}
func (t Trailer) Personalise() bool {
fmt.Println("personalising the trailer")
return true
}
func (t Trailer) CheckPlugConnection() bool {
fmt.Println("checking trailer plug")
return true
}
Next, we need to modify our Finish function so that products with electrical connections get this check performed on top of the other work we do in this function.
Because of composition, a type can implement any number of interfaces. This works in our favour because the function signature of Finish can remain unchanged. A Caravan, Trailer and WashingMachine all implement the Makeable interface, but additionally, they also implement the Pluggable interface.
Now we can use type assertion inside the Finish function to check if the type - which we know implements Makeable - also implements Pluggable. If it does we can call its CheckPlugConnection method conditionally.
We might use a type switch here if we had to test for multiple behaviours as we saw earlier when we were testing for specific concrete types, but for now we only need one a single if statement.
Let’s add this code to the Finish function. I’ve highlighted the new syntax in bold. Note the assertion: product.(Pluggable). The assertion is asking, “Is the product Pluggable?”, or, in better English, “Does the product implement Pluggable?”
func Finish(product Makeable) Makeable {
if ok := product.Test(); ok != true {
log.Fatalln("test failed")
}
if ok := product.Personalise(); ok != true {
log.Fatalln("personalisation failed")
}
// check if also implements Pluggable
pluggable, is := product.(Pluggable)
if is {
if ok := pluggable.CheckPlugConnection(); ok != true {
log.Fatalln("check of plug connection failed")
}
}
return product
}
The complete example is here: https://play.golang.org/p/omkL5Otl93u. Run it for yourself.
I’ve reproduced the output and highlighted the CheckPlugConnection operations below. Notice how, the check is not performed on the Car product, because Car does not implement Pluggable.
testing the car
personalising the car
testing the caravan
personalising the caravan
checking caravan plug
testing the trailer
personalising the trailer
checking trailer plug
testing the washing machine
personalising the washing machine
checking washing machine plug
Production finishing complete
Summary
That’s all I’ve got for you on Go interfaces.
I really hope my real-world scenario or fable, demonstrated the problem of working with concrete types when we know that change is likely and that, while it may be possible to process types in a similar way, it’s just as likely that we’ll need to process them in different ways too.
Interfaces give us flexibility. By choosing to work with abstract types, which we call interfaces, we can build our application around behaviours rather than absolutes; what something does, and not what something is.
This is often referred to as “duck-typing”. If it walks like a duck and quacks like a duck, then there’s no problem if our application logic treats it like a duck. Even if it’s a cow, that quacks, and shuffles along!
If you enjoyed my tutorial please share it with other aspiring Gophers and I encourage you and them, to subscribe to my newsletter on here. As a subscriber, you’ll receive my weekly #golang tutorials directly to your email inbox!
As always, comments and feedback are most welcome. Sadly, there is no 200% pay rise for you nor me, even though we did an exceptional job, but please, no complaints to me about that - you need to speak to the Boss.
Until next week!
Ollie.
I really like it. Interface is really about behavior classification and using dynamic dispatching at runtime.