Back

Interfaces in Go

Let’s dissect interfaces and become masters of the trade

by Percy Bolmér, January 25, 2021

By [Daniel McCullough] on Unsplash
By [Daniel McCullough] on Unsplash

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.

package main

import (
	"fmt"
)

type Human struct {
	Name string
}
// This is our method receiver, the type Human receives the Hello method
func (h Human) Hello() string {
	return fmt.Sprintf("Hello, %s!", h.Name)
}

func main() {
	h := Human{
		Name: "Bob",
	}
  // Since human has received the method Hello, we can call it
	fmt.Println(h.Hello())
}
Method receiver explained with example Try it at Playground

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.

package main

import (
	"fmt"
	"time"
	"errors"
)
// DateError is a custom error that will fulfill the Error interface
type DateError struct {
	Message string
	Date time.Time
}
// Our Method receiver where we Apply the Error() string function that the error interface needs to the DateError type
func (de DateError) Error() string {
	return fmt.Sprintf("%s: %s", de.Date.String(), de.Message)
}

func NewError(message string) DateError {
	return DateError{
		Message:message,
		Date: time.Now(),
	}
}
// PrintError can take in an DateError since DateError fulfills the error interface
func PrintError(err error) {
	fmt.Println(err.Error())
}

func main() {
	de := NewError("Auch, I failed")
	regularErr := errors.New("Another error")
	PrintError(de)
	PrintError(regularErr)
}
Showcase of using DateError and regular error in the same PrintError method Try it at Playground

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.
package main

import (
	"fmt"
	"time"
	"errors"
)

type DateError struct {
	Message string
	Date time.Time
}
// This part is updated so that we declare that the method receiver is only Pointers to DateError
func (de *DateError) Error() string {
	return fmt.Sprintf("%s: %s", de.Date.String(), de.Message)
}

func NewError(message string) DateError {
	return DateError{
		Message:message,
		Date: time.Now(),
	}
}
// PrintError can take in an DateError since DateError fulfills the error interface
func PrintError(err error) {
	fmt.Println(err.Error())
}
func main() {
	
	de := NewError("Auch, I failed")
	regularErr := errors.New("Another error")
	PrintError(de)
	PrintError(regularErr)
}
Updated example, with Pointer receiver instead. Try it at Playground
./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.


package main

import (
	"fmt"
	"time"
	"errors"
)

type DateError struct {
	Message string
	Date time.Time
}
// This part is updated so that we declare that the method receiver is only Pointers to DateError
func (de DateError) Error() string {
	return fmt.Sprintf("%s: %s", de.Date.String(), de.Message)
}
// NewError now returns an Pointer
func NewError(message string) *DateError {
	return &DateError{
		Message:message,
		Date: time.Now(),
	}
}
// PrintError can take in an DateError since DateError fulfills the error interface
func PrintError(err error) {
	fmt.Println(err.Error())
}
func main() {
	
	de := NewError("Auch, I failed")
	regularErr := errors.New("Another error")
	PrintError(de)
	PrintError(regularErr)
}
I’ve changed NewError to return an Pointer, it will still work Try it at Playground

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.

package main

import (
	"log"
	"errors"
)

func main() {
	// Create new MemoryDB 
	database := NewMemDB()
	// Add User
	err := database.AddUser("findMe", "findMe@nowhere.hidden")
	if err != nil {
		log.Fatal(err)
	}
	user, err := database.GetUser("findMe")
	if err != nil {
		log.Fatal(err)
	}
	
	log.Println(user)
}
// User is a data representation of our users
type User struct {
	Name string
	Email string
}
// MemoryDatabase is a user database that holds users in memory
type MemoryDatabase struct {
	Users map[string]*User
}

func NewMemDB() MemoryDatabase{
	return MemoryDatabase{
		Users: make(map[string]*User),
	}
}
// GetUser will print a user or an Error if not found
func (memDB *MemoryDatabase) GetUser(username string) (*User, error){
	// Search for User
	if user, ok := memDB.Users[username]; ok {
		return user, nil
	}
	return nil, errors.New("User does not exist")
}
// AddUser will add a user to the database
func (memDB *MemoryDatabase) AddUser(username, email string) error {
	if _, ok := memDB.Users[username]; ok {
    		return errors.New("User already exists")
	}
	memDB.Users[username] = &User{
		Name: username,
		Email: email,
	}
	return nil
}
The base of example, a memory DB. Try it at Playground

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.

package main

import (
	"log"
	"errors"
)

func main() {

	
	// Create new MemoryDB 
	database := NewMemDB()
	
	// create the API and apply the memory database to it
	api := API{
		db: database,
	}
	// Add User using the API instead
	err := api.db.AddUser("findMe", "findMe@nowhere.hidden")
	if err != nil {
		log.Fatal(err)
	}
	// fetch user will also still work using the interface
	user, err := api.db.GetUser("findMe")
	if err != nil {
		log.Fatal(err)
	}
	
	log.Println(user)
}
// UserDatabase is an interface that describes what's expected from structs that should be used as databases
type UserDatabase interface {
	GetUser(username string) (*User, error)
	AddUser(username,email string) error
}
// API is a struct that will act as an API and connect to a database
type API struct {
	db UserDatabase
}
// User is a data representation of our users
type User struct {
	Name string
	Email string
}
// MemoryDatabase is a user database that holds users in memory
type MemoryDatabase struct {
	Users map[string]*User
}

func NewMemDB() *MemoryDatabase{
	return &MemoryDatabase{
		Users: make(map[string]*User),
	}
}
// GetUser will print a user or an Error if not found
func (memDB *MemoryDatabase) GetUser(username string) (*User, error){
	// Search for User
	if user, ok := memDB.Users[username]; ok {
		return user, nil
	}
	return nil, errors.New("User does not exist")
}
// AddUser will add a user to the database
func (memDB *MemoryDatabase) AddUser(username, email string) error {
	if _, ok := memDB.Users[username]; ok {
    		return errors.New("User already exists")
	}
	memDB.Users[username] = &User{
		Name: username,
		Email: email,
	}
	return nil
}
The following gist shows how we use a Interface to reach the MemoryDatabase. Try it at Playground

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.

package main

import (
	"errors"
	"log"
)

func main() {

	// Create new testDB
	database := NewTestDB()
	// create the API and apply the memory database to it
	api := API{
		db: database,
	}
	// Add User using the API instead
	err := api.db.AddUser("findMe", "findMe@nowhere.hidden")
	if err != nil {
		log.Fatal(err)
	}
	// fetch user will also still work using the interface
	user, err := api.db.GetUser("findMe")
	if err != nil {
		log.Fatal(err)
	}

	log.Println(user)
}

// UserDatabase is an interface that describes what's expected from structs that should be used as databases
type UserDatabase interface {
	GetUser(username string) (*User, error)
	AddUser(username, email string) error
}

// API is a struct that will act as an API and connect to a database
type API struct {
	db UserDatabase
}

// User is a data representation of our users
type User struct {
	Name  string
	Email string
}

// TestingDatabase is a user database that always returns errors used for testing
type TestingDatabase struct {
	Users map[string]*User
}

func NewTestDB() *TestingDatabase {
	return &TestingDatabase{
		Users: make(map[string]*User),
	}
}

// GetUser will print a user or an Error if not found
func (testDB *TestingDatabase) GetUser(username string) (*User, error) {
	// Search for User
	return nil, errors.New("User does not exist")
}

// AddUser will add a user to the database
func (testDB *TestingDatabase) AddUser(username, email string) error {

	return errors.New("User already exists")
}

// MemoryDatabase is a user database that holds users in memory
type MemoryDatabase struct {
	Users map[string]*User
}

func NewMemDB() *MemoryDatabase {
	return &MemoryDatabase{
		Users: make(map[string]*User),
	}
}

// GetUser will print a user or an Error if not found
func (memDB *MemoryDatabase) GetUser(username string) (*User, error) {
	// Search for User
	if user, ok := memDB.Users[username]; ok {
		return user, nil
	}
	return nil, errors.New("User does not exist")
}

// AddUser will add a user to the database
func (memDB *MemoryDatabase) AddUser(username, email string) error {
	if _, ok := memDB.Users[username]; ok {
		return errors.New("User already exists")
	}
	memDB.Users[username] = &User{
		Name:  username,
		Email: email,
	}
	return nil
}
To exchange the whole API’s logic, I only needed to change the Database field into a test DB. Try it at Playground

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.

package main

import (
	"encoding/json"
	"log"
)

func main() {

	testData := []byte(`{
  "user":"test",
  "email":"test@testersson.test"
}`)

	var user User
  // Here we pass our User into unmarshal, it takes an empty interface, so it doesn't matter what type it is
	err := json.Unmarshal(testData, &user)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("Found user, ", user)
}

type User struct {
	User  string
	Email string
}
Example of what the empty interface can be used for. Try it at Playground

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.

// NewNetworkPayload is used to convert a regular payload into a network payload
func NewNetworkPayload(pay Payload) (*NetworkPayload, error) {
	conv, ok := pay.(*NetworkPayload)
	if ok {
		return conv, nil
	}
	return nil, ErrPayloadIsNotANetworkPayload
}
Type asserting payloads into NetworkPayloads.

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.

package main

import (
	"log"
)

func main() {

	user := User{
		Name:  "hello",
		Email: "world@worldly.com",
	}

	WriteLog(user)

}

// WriteLog accepts any kind of objects and logs them
func WriteLog(data interface{}) {
	switch result := data.(type) {
	case User:
		log.Println("Found user: ", result.Name)
	default:
		log.Println(data)

	}

}

type User struct {
	Name  string
	Email string
}
Example of how to use type switches Try it at Playground

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.

package main

import (
	"errors"
	"log"
)

func main() {

	// Create new MemoryDB
	database := NewMemDB()

	// create the API and apply the memory database to it
	api := API{
		db: database,
	}
	// Add User using the API instead
	err := api.db.AddUser("findMe", "findMe@nowhere.hidden")
	if err != nil {
		log.Fatal(err)
	}
	// fetch user will also still work using the interface
	user, err := api.db.GetUser("findMe")
	if err != nil {
		log.Fatal(err)
	}

	log.Println(user)
}

// UserDatabase is an interface that describes what's expected from structs that should be used as databases
type UserDatabase interface {
	UserRetriever
	AddUser(username, email string) error
}

// UserRetriver is a broken out interface that will be embedded by UserDatabase
type UserRetriever interface {
	GetUser(username string) (*User, error)
	GetUserByEmail(email string) (*User, error)
}

// API is a struct that will act as an API and connect to a database
type API struct {
	db UserDatabase
}

// User is a data representation of our users
type User struct {
	Name  string
	Email string
}

// MemoryDatabase is a user database that holds users in memory
type MemoryDatabase struct {
	Users map[string]*User
}

func NewMemDB() *MemoryDatabase {
	return &MemoryDatabase{
		Users: make(map[string]*User),
	}
}

// GetUser will print a user or an Error if not found
func (memDB *MemoryDatabase) GetUser(username string) (*User, error) {
	// Search for User
	if user, ok := memDB.Users[username]; ok {
		return user, nil
	}
	return nil, errors.New("User does not exist")
}

// GetUserByEmail will fetch users by email instead
func (memDB *MemoryDatabase) GetUserByEmail(email string) (*User, error) {
	for _, user := range memDB.Users {
		if user.Email == email {
			return user, nil
		}
	}
	return nil, errors.New("User does not exist")
}

// AddUser will add a user to the database
func (memDB *MemoryDatabase) AddUser(username, email string) error {
	if _, ok := memDB.Users[username]; ok {
		return errors.New("User already exists")
	}
	memDB.Users[username] = &User{
		Name:  username,
		Email: email,
	}
	return nil
}
An example of how we now embed the UserRetriever interface. Try it at Playground

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

package main

import (
	"fmt"
)

func main() {
	
	user := &User{
		Name: "Brandon",
		Email: "Sandersson",
	}
	
	Print(user)
}

// Print accepts any printer and runs their Print function
func Print(p Printer) {
	p.Print()
  // uncomment p.Rewind to see whats wrong with this
	// p.Rewind()
}

type Printer interface {
	Print()
	Rewind()
}

type User struct {
	Name string
	Email string
	Printer
}

func (u *User) Print() {
	fmt.Println("User: ", u.Name)
}
Gist showing how dangerous a embedding can be Try it at Playground

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.

An article where we walk through the basics of docker
Docker Skyrocketed My Teams Productivity

January 16, 2021

An article where we walk through the basics of docker

Read more

If you enjoyed my writing, please support future articles by buying me an Coffee

Sign up for my Awesome newsletter