In this post I will describe the Preemptive Interface pattern often used in code and why I think it is generally an incorrect pattern to follow in Go.

What is a Preemptive Interface Link to heading

Interfaces are a way to describe behavior and exist in most typed languages. Preemptive interfaces are when a developer codes to an interface before an actual need arrises. An example may look like the following.

type Auth interface {
    GetUser() (User, error)
}
type authImpl struct {
    // ...
}
func NewAuth() Auth {
    return &authImpl
}

When preemptive interfaces are useful Link to heading

Preemptive interfaces are often used with much success in Java, which is where I believe most Go programmers get the idea that it is also good practice in Go. The driving difference that makes this not true is that Java has explicit interfaces while Go has implicit interfaces. Let’s look at some example Java code that shows the difficulties that can arise if one does not use preemptive interfaces in Java.

// auth.java
public class Auth {
    public boolean canAction() {
        // ...
    }
}
// logic.java
public class Logic {
    public void takeAction(Auth a) {
        // ...
    }
}

Now suppose you want to change Logic::takeAction to take any object, as long as it has a canAction() function. Unfortunately, you cannot. Auth does not implement an interface with canAction() inside it. You now have to modify Auth to give it an interface that you can then accept in takeAction, or wrap Auth in a class that does nothing other than implement your method. Even if logic.java defines an Auth interface to accept in takeAction(), it could be very difficult to make Auth implement that interface. You may not have access to modify Auth, or Auth could be in a third party library that is a pain to fork. Maybe the author of Auth doesn’t agree with your interface. Maybe you share Auth with coworkers in the codebase and now you need consensus before modifying it. Here is what you wish the Java code was.

// auth.java
public interface Auth {
    public boolean canAction()
}
// authimpl.java
class AuthImpl implements Auth {
}
// logic.java
public class Logic {
    public void takeAction(Auth a) {
        // ...
    }
}

If the author of Auth had originally coded to and returned an interface, you never would have ran into a problem trying to extend takeAction. It naturally works with any Auth interface. In languages with explicit interfaces, your future self will thank your past self for using preemptive interfaces.

Why this is not an issue in Go Link to heading

Let’s setup the same situation in Go.

// auth.go
type Auth struct {
    // ...
}
// logic.go
func TakeAction(a *Auth) {
    // ...
}

If logic wants to make TakeAction generic, the owner of logic can take this action unilaterally without disturbing others.

// logic.go
type LogicAuth interface {
    CanAction() bool
}
func TakeAction(a LogicAuth) {
    // ...
}

Notice that auth.go does not need to change. This is the key here that makes preemptive interfaces unneeded.

Unintended side effects of preemptive interfaces in Go Link to heading

Go is at its most powerful when interface definitions are small. In the standard library, most interface definitions are a single method. This allows the greatest reuse because implementing an interfaces is easy to do. When programmers code to preemptive interfaces like Auth above, the interfaces tend to explode in method count, which makes the whole point of an interface (exchangeable implementations) even more difficult to accomplish.

Best usage for interfaces in Go Link to heading

A great rule of thumb for Go is accept interfaces, return structs. Accepting interfaces gives your API the greatest flexibility and returning structs allows the people reading your code to quickly navigate to the correct function.

Even if your Go code accepts structs and returns structs to start, implicit interfaces allow you to later broaden your API without breaking backwards compatibility. Interfaces are an abstraction and abstraction is sometimes useful. However, unnecessary abstraction creates unnecessary complication. Don’t over complicate code until it’s needed.