Leveraging local interfaces in Go

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:

  • By depending on a set of methods, we only depend on the contract expressed by these methods, and not on any implementation details. This ensures that we don’t accidentally couple our code tightly to certain implementation details in code we haven’t audited or written ourselves.

  • By declaring interfaces for our dependencies, we make hidden assumptions explicit. An object you use might have dozens of methods and you are unlikely to use all of them at the same time in the same place. By using an interface you clearly communicate which handful of methods you need.

  • Because interfaces serve as seams between two parts, we can easily change one part without the other part knowing about it. If your code only depends on a key-value store, you can swap out the concrete implementation for any key-value store without your code having to change. When the cost of changing technologies is low, you can change your mind about what you want to use later, when you know more about your problem.

  • As a side-effect of having a seam in place, it becomes trivial to provide a specific implementation of an interface for testing purposes. Luckily the convention in Go is to make errors part of a function’s signature, so it feels natural to create test implementations that returns errors on purpose.

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:

  • dependencies are captured clearly in the interfaces used for the various collaborators,

  • testing is easier because only a few methods (usually just one per collaborator) need to be implemented for a test double,

  • (subjective) the code becomes easier to understand, because the implementation of the core algorithm is spelled our clearly.

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:

  • an object for finding users by email address,
  • an object for generating password reset tokens for a user,
  • and an object for sending “password forgotten” emails.

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.


Want to try Harrow.io? Start immediately with a 14 day free trial or learn more

Harrow.io is a platform for Continuous Integration, testing and deployment, built by the team behind Capistrano.
Add powerful automation and collaboration capabilities to your existing tools.

  • Automate any software, in any language.
  • Create self-documenting, repeatable web-based tasks
  • Make them available for your team-mates
  • Trigger tasks automatically based on Git changes and webhooks.
  • Get notified by email, slack, etc.
  • Free for small projects

Test, deploy and collaborate online easily, using tools you already know and love.
Works seamlessly for PHP, Node.js, Ansible, Python, Go, Capistrano and more!

Learn more Start your 14 day free trial

No credit card required – completely free for small or public projects.

Engineer with a passion for all things unusual, functional and fast. Dario wields tools from TCL to Prolog to get things done in ways that wouldn't occur to most people.

Start Using Harrow Today!

Harrow is flexible, powerful and can make your team much more efficient.

Start free trial