Leveraging package-local interfaces in Go

Observations about how interfaces are commonly used in Go and a suggestion on how to keep interfaces narrow.

This article is about leveraging interfaces local to your packages in Go to protect your future self from incidental complexity -- problems you've created for yourself, that get in the way of solving your actual problems -- when developing an application. The examples used in this article are within the context of a typical web application.

Coming from Ruby on Rails, writing a web application in Go just based on net/http can be a daunting task: Rails provides organizationial principles for your code and comes with lots of goodies for making your life easier. Go's net/http is the complete opposite of this: it only takes care of handling HTTP requests and doesn't prescribe or help you with other tasks you face when developing a web application.

While this might seem like a disadvantage for Go at first, it is actually a big strength. Not having automatic defaults in place for everything forces you to think a bit about what you actually need for your application to do its job. Making these dependencies explicit in the code reduces the effort of updating or changing dependencies to a minimum. If you have ever had to upgrade a Rails application using a lot of gems from one Rails major version to another, you know how much unthankful work third-party dependencies can cause.

Luckily Go comes equipped with a tool for easily managing dependencies in your code: interfaces. A helpful working definition of an interface is that it is a set of methods . This is the definition we are going to use for the remainder of this article. The Go Language Specification has an exact explanation of what interfaces are.

So how does having sets of methods help us with managing dependencies? Well, if we can express the things our code depends on as a set of methods and then our code can work with any value that implements those methods. This is useful for several reasons:

This list of benefits looks quite compelling. Unfortunately reality often looks different and in spite of something being defined as an interface, none of the mentioned benefits are seen.

Observations in practice

Wide interfaces

In practice it looks like interfaces are often only introduced when people feel the need for providing more than one implementation of a certain thing. When this happens, the interface is usually defined in the same package as the various implementations of that interface.

Let's look at a concrete example of how this happens: you want to integrate a third-party service with an HTTP API into your project, say a payment gateway. You check the documentation of the payment gateway and come up with this interface, in your new package payment:

type Gateway interface {
	CreateCustomer(customer *Customer) error
	Customers() ([]*Customer, error)
	PaymentMethods(customer *Customer) ([]*PaymentMethod, error)
	Charge(customer *Customer, amount int) error

This interface reflects the actions exposed by the payment provider's API and next to that interface you have a concrete implementation that talks to the payment provider via its API.

Next you write some code that makes use of this interface:

type PlaceBookingController struct {
	Payments payment.Gateway
	// other details omitted

func (controller *PlaceBookingController) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// ...

	if err := controller.Payments.Charge(customer, booking.Price); err != nil {
		controller.handleError(w, req, err)

When using this controller, you need to provide an object that implements payment.Gateway -- four methods! -- even though only one is used here. This causes one issue right now and another one a bit further down the line:

  1. when testing PlaceBookingController, your test implementation of payment.Gateway needs to support all four methods, even though only one is used;
  2. every time any user of the interface needs a new method, the interface grows. This tendency to grow makes it hard to provide alternative implementations for an interface, which need to implement all of those methods.

Tightly coupled dependencies

Another thing that can often be observed, is directly referring to third-party libraries in your own code, thus tighly coupling your own code with the public interface of that library. Why is that bad? Every time there is an update to that library, you risk breaking lots of places in your code -- potentionally every place in which you refer to that library. The obvious way to mitigate that risk is to carefully update your dependencies when you have set time aside for doing just that. As you might have experienced already, this rarely happens, which leaves you with a bunch of outdated dependencies that miss all security related updates.

But what if you could update dependencies casually? What if any breaking changes would be isolated to only one small part of your code base? This can be achieved by introducing a thin layer -- implemented using interfaces -- between your code and the third-party code you want to isolate yourself from. Martin Fowler provides a great, detailed example in Ruby for how this looks in practice here

One change, many benefits

Having looked at some of the problems in the detail, we can now look at one possible solution for addressing them. For the remainder of this article, we'll call the solution "local interfaces". As it commonly happens, it is easiest to apply this technique to new code, but changing existing code for this to work should be straight forward as well.

In a gist, here's the technique:

  1. start by writing the high-level algorithm, making up methods as you go,
  2. for every method you made up in the first step, think about whether this object can implement them, or whether you need to introduce a collaborator for providing that method,
  3. for every collaborator needed, introduce an interface in the package of the object that needs the collaborator .

Applying this techniques yields a couple of benefits:

Go gets to show its strengths here: because types do not need to explicitly declare that they implement an interface, often it is possible to just use already existing types as collaborators.

Example: Password reset

The explanation of how to use "local interfaces" can be hard to visualize, so we'll go through a step-by-step example here. For this example we will look at resetting a user's password. The flow for this feature is:

  1. The user submits an email address to the password reset endpoint.
  2. A lookup against the database is performed to see whether the email address belongs to a registered user -- if it doesn't, we return early.
  3. A password reset token is generated and associated with that user.
  4. An email is being sent to the address from step 1, containing a link to the page for resetting the password. This link includes the token from step 2.

Let's call object implementing this behavior the ForgotPasswordController. We start with a straight translation of the algorithm from English into Go:

type ForgotPasswordController struct{}

func (controller *ForgotPasswordController) ForgotPassword(emailAddress string) error {
	existingUser, err := controller.users.FindByEmail(emailAddress)

	if existingUser == nil {
		// by returning a success, we don't disclose to a potential //
		// attacker whether the email address belongs to a registered user
		// or not.
		return nil

	if err != nil {
		return err

	token, err := controller.passwordResetTokens.GenerateFor(existingUser)
	if err != nil {
		return err

	return controller.emails.SendPasswordForgotten(emailAddress, existingUser, token)

This was a straight copy of the description of our task in English. We've pretended that we have access to three objects that make our life easier:

All of these yield straighforward, narrow interfaces:

type Users interface {
	FindByEmail(emailAddress string) (*User, error)

type PasswordResetTokens interface {
	GenerateFor(user *User) (*PasswordResetToken, error)

type Emails interface {
	SendPasswordForgotten(emailAddress string, user *User, token *PasswordResetToken) error

We provide these dependencies when creating a new instance of the ForgotPasswordController:

type ForgotPasswordController struct {
	users               Users
	passwordResetTokens PasswordResetTokens
	emails              Emails

func NewForgotPasswordController(users Users, passwordResetTokens PasswordResetTokens, emails Emails) *ForgotPasswordController {
	return &ForgotPasswordController{
		emails:              emails,
		passwordResetTokens: passwordResetTokens,
		users:               users,

How the concrete implementation of those interfaces looks like is left to the reader -- it depends on the rest of the system, i.e. which database is being used, how emails are sent in general, etc. Providing implementations for unit tests is pretty straighforward and doesn't depend on the rest of the system. Here's the test for checking that we don't accidentally send out an email if no user is found:

func ForgotPasswordController_ForgotPassword_does_not_send_email_if_no_user_is_found(t *testing.T) {
	emails := NewEmailsInMemory()
	users := NewUsersInMemory()
	tokens := NewPasswordResetTokensInMemory()
	controller := NewForgotPasswordController(users, tokens, emails)

	if err := controller.ForgotPassword("john.doe@example.com"); err != nil {
		t.Fatalf("Expected no error to be returned, got: %s", err)

	if sentEmails := emails.Sent(); len(sentEmails) > 0 {
		t.Fatalf("Email[s] sent to: %#v", sentEmails)

type EmailsInMemory struct {
	sent []string

func NewEmailsInMemory() *EmailInMemory {
	return &EmailsInMemory{
		sent: []string{},

func (emails *EmailsInMemory) Sent() []string {
	return emails.sent

func (emails *EmailsInMemory) SendPasswordForgotten(emailAddress string, user *User, token *PasswordResetToken) error {
	emails.sent = append(emails.sent, fmt.Sprintf("password-forgotten:%s:%s", token, emailAddress))
	return nil

This test will run fast and works on a comfortably high level of abstraction. The speed comes from being able to use in-memory versions of all dependencies, instead of having to send out an actual email, talking to an actual database, etc. As an additional bonus, we can get proper integration tests by substituting the "real" implementations that are being used in production for all dependencies.

And that is really all there is to it! Having such narrowly scoped interfaces is not all that different from using callbacks. The interfaces just provide an additional opportunity to name things for clarity and the objects implementing them provide a convenient hook for managing any necessary state.