Using Interfaces in Go

Taylor BrownTaylor Brown
6 min read

Learning how to use interfaces in Go will help you write flexible and simpler code. When I was first learning Go I found the posts about interfaces to be lacking. The examples weren't practical (Stringer, Geometry, Animal - these posts are great, but I write http services and grokking the general concept wasn't as easy with these examples) and some straight up missed the important reasons to use them. I'm hoping to fix that with this blog post :)

In this post we'll make an interface for interacting with a database and use it to write some tests. It's much simpler than it sounds.

What is an interface?

You can think of an interface as a bag of functionality. It's a set of methods that something must implement if it wants to be that interface.

Why is it useful for me?

Interfaces allow you to switch implementations without changing the code that uses your interface. This can help when the underlying implementation needs to change - maybe you're switching databases. It can also be useful for testing. You can provide mocked implementations instead of the real thing. We'll go through some examples to build some intuition for using them.

Example

Let's start with a hypothetical service that will take in a userId and return a bool indicating whether the user is old enough to buy alcohol. We'll pretend that our users are stored in some database and we'll need to retrieve the User model before we can say yes or no.

type User struct {
    id   string
    name string
    age  int
}

type UserRepository interface {
    Get(id string) (User, error)
}

Here we have our User struct and an interface that represents our repository. We have a pretty good idea of what methods need to exist so I wrote the interface before even implementing the code. Now let's implement it.

type inMemoryUserRepository struct {}

func (r *inMemoryUserRepository) Get(id string) (User, error) {
    return User{"User", "name", 14}, nil
}

For simplicity, I am not going to connect to a real database. To satisfy an interface a struct must implement the methods that exist for the interface. Anything that implements the interface methods can be provided wherever the interface is requested. This is an important concept! You'll notice that libraries do not provide interfaces for their functionality. Instead, they leave that up to you. We'll provide an example of this later, but let this notion sink in.

In this example, we have a repository that we can use to implement our legal age method.

type userAgeService struct {
    userRepo UserRepository
}

func NewUserAgeService(userRepo UserRepository) userAgeService {
    return userAgeService{userRepo}
}

func (r *userAgeService) CanBuyAlcohol(id string) (bool, error) {

    user, err := r.userRepo.Get(id)
    if err != nil {
        return false, err
    }

    if user.age < 21 {
        return false, nil
    }

    return true, nil
}

We've created a little service that can answer our question now. The userAgeService does not care what kind of UserRepository is provided. It only cares that it is provided one. It could be a Postgres repository, an in-memory one, Redis, MySQL, or whatever... This service functions the same regardless.

This is an example of "separation of concerns", "dependency injection", "composition", "modularity", "reusability", etc... Scary words for a simple concept. The complexity of connecting to a database, querying and error handling is offloaded to the UserRepository so that userAgeService can focus on implementing the logic for buying alcohol.

There are a lot of benefits to using interfaces but an obvious one is for testing.

package main

import (
    "testing"
)

type youngUserRepository struct{}

func (r *youngUserRepository) Get(id string) (User, error) {
    return User{"123", "Name", 1}, nil
}

type adultUserRepository struct{}

func (r *adultUserRepository) Get(id string) (User, error) {
    return User{"123", "Name", 30}, nil
}

func TestLogUser(t *testing.T) {

    t.Run("Check if a 1 year old can buy alcohol", func(t *testing.T) {
        userLogger := NewUserAgeService(&youngUserRepository{})

        got, _ := userLogger.CanBuyAlcohol("123")

        if got != false {
            t.Errorf("A one year old should not be able to buy alcohol, oops!")
        }
    })

    t.Run("Check if adult can buy alcohol", func(t *testing.T) {
        userLogger := NewUserAgeService(&adultUserRepository{})

        got, _ := userLogger.CanBuyAlcohol("123")

        if got != true {
            t.Errorf("An adult should be able to buy alcohol")
        }
    })
}

When we test our userAgeService we're concerned about whether it will correctly deny a minor while allowing an adult. Since we're using the UserRepository interface we can create mocks that will allow us to test each scenario. Notice how we don't have to mess with the inMemoryUserRepository or anything related to it. Instead, this file is self-contained, and won't need to change regardless of how the UserRepository code may change in the future.

Interfaces for libraries

We can't end our discussion about interfaces without talking about how to use them with a library. As you look through Golang libraries an interface is typically not defined for you. This confused me at first - I wanted to use that interface so I could mock out the library functionality when I was testing. Turns out it's on you to define the interface for the library functionality you'll be using, so you can do your mocking.

An interface can be satisfied by any piece of code, even a library. All you need to do to make your library code mockable is to define an interface for it.

Let's say you're pulling in a library that allows you to query for subreddits on Reddit.

// This code comes from:
//  https://github.com/vartanbeno/go-reddit/blob/master/reddit/subreddit.go
// I don't endorse this project, the creator, or Reddit. It's a nice, relateable example :) 
type SubredditService struct {
    client *Client
}

func (s *SubredditService) getPosts(ctx context.Context, sort string, subreddit string, opts interface{}) ([]*Post, *Response, error) {
    // removed the code because it was taking too much space but stuff happens here...
}

func (s *SubredditService) TopPosts(ctx context.Context, subreddit string, opts *ListPostOptions) ([]*Post, *Response, error) {
    return s.getPosts(ctx, "top", subreddit, opts)
}

func (s *SubredditService) NewPosts(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) {
    return s.getPosts(ctx, "new", subreddit, opts)
}

func (s *SubredditService) RisingPosts(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) {
    return s.getPosts(ctx, "rising", subreddit, opts)
}

func (s *SubredditService) ControversialPosts(ctx context.Context, subreddit string, opts *ListPostOptions) ([]*Post, *Response, error) {
    return s.getPosts(ctx, "controversial", subreddit, opts)
}

Let's say you want to use this code but you only want to get the TopPosts(...). Since you don't care about all the other functionality, it'd be nice to ignore those other methods when you're testing. This is exactly why the library does not expose an interface. The library doesn't know what set of functionality you want to use. If they defined an interface it'd contain all the methods but you only need one. You can define your own!

type MySubredditInterface struct {
    TopPosts(ctx context.Context, subreddit string, opts *reddit.ListPostOptions) ([]*reddit.Post, *reddit.Response, error)
}

That's it. Your code can refer to the service it needs as MySubredditInterface instead of the Reddit library's SubredditService. The library's SubredditService implements MySubredditInterface, so you can pass the SubredditService anywhere that requires a MySubredditInterface. You can now mock out any library you use. You will use this pattern a lot.

------------

If you read all of this, thank you! I hope it was helpful. All feedback is appreciated, even - and especially - if the post sucked.

2
Subscribe to my newsletter

Read articles from Taylor Brown directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Taylor Brown
Taylor Brown