Interfaces in Go
Let’s dissect interfaces and become masters of the trade
by Percy Bolmér, January 25, 2021
I feel obliged to make an article about interfaces since it’s one of the features in Go that amazed me the most .
I’m not gonna lie, it took me a while to get comfortable using them, but when I passed the learning threshold I started loving them.
It’s not that Interfaces are hard or complex, but learning the habit to use them and the mindset seem to confuse many.
In this article I will try to use a lot of examples so that we not only learn how to use them, but also when.
Inter — What now?
Let’s start at the beginning and review the basics before we move on. An interface is a collection of method receivers.
What that means is that we define a rule, that any type that has ALL methods described, is part of an interface. It might be worth reminding people what a method receiver is as well. It’s when we apply a method to a type. The gist below shows an example, this works for both pointers and non-pointers.
Declaring an interface only creates a blueprint of what methods are needed to be part of it. You don’t describe WHAT each method does, only how they will behave. You declare that any type that has a Method called String()
that outputs a string is part of the Stringer
interface. But the actual implementation of what string is outputted is up to the developer. This becomes powerful when properly used because instead of rewriting many different functions, we can write a function that accepts certain interfaces.
If a type has all the described methods in the interface, it’s automagically considered part of the interface, no need to define it!
The standard library has a few usable interfaces defined. Let’s use one of them as an example to better understand. We will take a look at the Error
interface.
If you never knew the error was an interface, don’t be ashamed, it took me a long while to realize.
type error interface {
Error() string
}
With error, the standard library declares that ANY type that has a method called Error , and takes no input arguments, and outputs a string is part of the error interface.
This means that we can make any of our structs work like an error if we implement the Error() string
method.
This could prove useful if we wanted to extend information sent in an error. I’ve usually seen this done when people want to add extra information to errors in HTTP etc.
In the gist I’ve created a DateError
type, it is a very simple struct that will append the current date when calling the Error()
method. This DateError
could be passed in and out of functions as an error
since it has the Error() string method, making it part of the error interface.
I think it’s time to also mention that there is a difference between a pointer and a nonpointer when defining a method receiver.
When declaring the method we have to say if it’s for regular structs or only pointers to them that will receive the method. If we update the `Error` method to only apply on pointers, we should see a build failure../prog.go:32:12: cannot use de (type DateError) as type error in argument to PrintError:
DateError does not implement error (Error method has pointer receiver)
This is a common thing that I see is missed. So it’s important to remember that you always think about if it should apply to pointers or not. Usually, pointers should be used when your method will modify the object directly instead of returning a value. It can also be used to avoid passing copies of objects around to spare the amount of memory used.
One thing to remember is that interfaces are always passed as a pointer , even if your struct is not. I will showcase this in an example to make it more clear.
In the gist above I’ve modified NewError
to output a Pointer to a DateError
instead. It will still work, since when passing our DateError
into PrintError
it will be passed as a pointer. Interfaces are always passed as pointers.
Why its useful
When I started using Go I didn’t use interfaces for a long time, at least I wasn’t aware of using them, as an error.
Since learning to use them more frequently I’ve made my code more modular, easier refactored and simplified testing. That’s why I find it important to discuss why they are useful. I will use examples to show and strengthen my argument.
The first point I’d like to address is modularity . I like being able to switch out pieces of my code easily. In smaller applications, this usually doesn’t mean a lot of refactoring, but in large projects, this can be harsh and break a lot of things.
Interfaces allow us to skip the pain of refactoring and very smoothly exchange business logic. So let’s see a UserDatabase
example. Here we will create a small interface that handles Users. During the showcase, it will be using memory as storage, and use an interface to show how we can easily replace it without breaking the application.
The example is the following super easy command-line tool which will print out the user information. In real life, this could be an HTTP API etc.
I will use the above gist as a base for our refactor. We will be by modifying it and add a UserDatabase
interface. This will allow us to easily swap the whole memory database at a later stage, without breaking other processes that rely upon it. To begin we will implement the Interface, and also a struct called API
which will hold a UserDatabase
.
The point of the API struct is to show how things don’t break, our example will only print to stdout, but In real cases, it could be an API that serves an HTTP endpoint, or anything.
When creating an interface, It’s good practice to try to think about what methods have to be part of it, somebody once said my interfaces should contain a maximum of 5–6 methods.
It’s a lot easier to work with smaller interfaces, and if it grows too big, splitting it into multiple interfaces.
We will want AddUser
and GetUser
for the interface we create.
We have now added an Interface that our API uses. Let’s try replacing the database with something else to see how modular our example becomes.
The database I will exchange into is a Testing database that always returns errors, It’s because I want to lead you into my next argument why to use Interfaces.
Testing becomes easier
Another really good part of interfaces is that they can help with testing. Some methods can be easy to test, but some are harder since they run methods that depend on other methods and services.
That’s when interfaces can help to test. We can replace services with our struct that behaves as we need it to during the test.
Say if there was an HTTP handler we wanted to test how the behavior as if the user didn’t exist, we could apply the TestDatabase
we created instead.
DRY (Don’t repeat yourself)
Interfaces allow us to not repeat code. We can use interfaces to pass multiple structs into the same function where we want the same behavior. I’ve got a great example of this, in a data processing project I have.
I’m building a data processing tool that performs certain actions and transfers the data between many processors, such as reading a file, parsing csv, or logging it. I started the project by passing a regular []byte
between each processor. This turned out to not work, you see, I wanted the processors to be seamless. Passing a []byte
required the next processor to know how to unravel the []byte
into usable data.
The solution was interfaces. I’ve created an interface called Payload
. Payload is a simple interface containing a GetPayload() []byte
method. At first glance, this may seem to not help at all, but it allows the processors to pass data seamlessly, it does not matter if it’s a CsvPayload, JsonPayload, etc.
Tips and Tricks
This part of the article will cover some tips and tricks that can be utilized with interfaces. I’m gonna kick start it with the most common trick, the empty interface.
Empty interface
The empty interface is by far the most powerful tool when it comes to interfaces according to me. So what is it? It’s an interface, that has 0 methods applied to it, making every type part of it. This allows you to send ANY type into a method.
This is very common to find in Go codebases. An example is when we unmarshal JSON. If we look at the JSON.Unmarshal, you can see that it’s a method defined as the following.
func Unmarshal(data []byte, v interface{}) error
See the interface{}
It’s how you use an empty interface.
What we do there is say “Any type that has all these 0 methods, can be passed in”.
This means we can pass any kind of data into this method.
One thing to remember when working and using the empty interface is
With great power comes great responsibility
Just because a method can accept every type, doesn’t mean it will work correctly with any type.
Type assertion
You can perform type assertion on Interfaces. This is useful when you want a function to accept two kinds of data but still want to separate what’s done based on the input.
An example is my data processing tool I mentioned earlier, some processors expect network payloads. In there I have to accept the payload interface since it helps the framework be standardized, but I still need to validate the correct input type.
So my framework will have Payload
as input (which is an interface), but these certain processors need the Payloads to be converted into NetworkPayloads
. At that point, you can use a type assertion that looks like the Gist below.
Notice the pay.(*NetworkPayload)
, that’s how a type assertion looks like in Go.
YourType.(WantedType)
Type assertion will return two things, one type of the wanted type, and one boolean that indicates if it could successfully convert the Type.
This is great for when you need to access the underlying data that isn’t exposed by the interface itself. See the next example in type switching how I use the result. Name to access a data field that isn’t exposed by the empty interface.
Type Switching
So maybe we want to have a method that accepts an empty interface but decides what route to take depending on what kind of type it is. You can use type assertion to convert a type, but type switching allows you to test multiple types.
It’s usually used to avoid duplicating code, say your application should log different events. We can write a standardized log method that takes in any type and logs differently based on the type.
Embedding interfaces
In Go, you can also embed interfaces into other interfaces, or structs. Embedding an interface in another interface means that the interface embedding another will also require the inherited methods.
An example might be easier. I’m going to show this on the UserDatabase
interface we created earlier. The reason behind embedding (at least for me) is that we can keep the code more simple and again, modular. Imagine we now want to also fetch users by email, so we need to add that.
type UserDatabase interface {
GetUser(username string) (*User, error)
GetUser(email string) (*User, error)
AddUser(username, email string) error
}
Now we got 2 methods that does the same, but in a real application, this could be many more. Let’s split that out into its own interface and embed it.
Warning, brace yourself, big GIST incoming.
I have to mention it, but I don’t like it. It’s possible to embed interfaces into structs, but I have to warn you, it’s very error-prone.
I’ll just make a short example of it, to show you how it works, but also what’s dangerous about it. Embedding an interface into a struct will tell the compiler that, yes, this type fulfills the interface EVEN IF IT DOESN’T
Try uncommenting line 20 in the playground, and see what happens. It will compile, but since the User
struct isn’t a Printer
, it will Panic.
Sadly, we have reached an end on this journey.
Hopefully, you’ve learned more about interfaces. Be sure to reach out to me if there is anything you want to discuss, flame, or want me to write about.
If you want some extra practice, I recommend taking the Memory database example and try replacing it with a PostgreSQL database for testing.
That will probably be a good training exercise.
You can setup PostgreSQL easily to use in development with Docker, read more about that in my article about Docker.
Docker Skyrocketed My Teams Productivity
January 16, 2021
An article where we walk through the basics of docker
Read moreIf you enjoyed my writing, please support future articles by buying me an Coffee