Both use interfaces. Why is there such a big gap between Java and go?

Time:2021-11-28

This article will describe the preemptive interface pattern often used in code and why I think it is usually incorrect to follow this pattern in go.

What is a preemptive interface

Interface is a way to describe behavior, which exists in most type languages. Preemptive interface means that developers code the interface before the actual needs appear. An example might look like this.

type Auth interface {
  GetUser() (User, error)
}

type authImpl struct {
  // ...
}
func NewAuth() Auth {
  return &authImpl
}

When is a preemptive interface useful

Preemptive interfaces are often used in Java and are very successful, which is the idea of most programmers. I believe many go developers think so. The main difference in this usage is that Java has an explicit interface, while go is an implicit interface. Let’s look at some sample java code that shows the difficulties that can occur if you don’t use the preemptive interface 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 the parameter auth type object in the takeaction method of logic, as long as it has the canaction () method. Unfortunately, you can’t. Auth does not implement an interface with canaction() in it. You must now modify auth to provide an interface, and then you can accept the interface in takeaction, or wrap Auth in a class that does nothing except the implemented method. Even if logic.java defines an auth interface to accept in takeaction (), it may be difficult for auth to implement the interface. You may not have permission to modify auth, or auth may be in a third-party library. Maybe the author of auth doesn’t agree with your modification. Perhaps sharing auth with colleagues in the code base now requires consensus before modification. This is the desired java code.

// 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 initially encodes and returns an interface, you will never encounter problems when trying to extend takeaction. It naturally applies to any auth interface. In languages with explicit interfaces, you will thank yourself for using preemptive interfaces in the past.

Why is this not a problem in go

Let’s set the same situation in go.

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

If logic wants to make takeaction generic, the logic owner can do it unilaterally without disturbing others.

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

Note that auth.go does not need to be changed. This is the key to making preemptive interfaces unnecessary.

Unexpected side effects of preemptive interfaces in go

Go’s interface definitions are small but powerful. In the standard library, most interface definitions are single methods. This allows maximum reuse because the interface is easy to implement. When programmers code preemptive interfaces such as auth above, the number of interface methods tends to surge, which makes the full meaning of the interface (exchangeable Implementation) more difficult to realize.

Best use of interfaces in go

A good rule of thumb for go is – accept the interface and return the structure. The accept interface provides maximum flexibility for your API, and the return structure allows the caller to quickly navigate to the correct function.

Even if your go code accepts the structure and returns it to start, the implicit interface allows you to extend your API later without breaking backward compatibility. An interface is an abstraction, which is sometimes useful. However, unnecessary abstraction can cause unnecessary complexity. Don’t complicate your code until you need it.