Using Interfaces in Go
Learning how to use interface
s in Go will help you write flexible and simpler code. When I was first learning Go I found the posts about interface
s 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 interface
s 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 interface
s 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 interface
s 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.
Subscribe to my newsletter
Read articles from Taylor Brown directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by