Rules of Interfaces in Go

Go is a new language. The direct translation of programs from other languages like Java does not produce quality and satisfactory code. Go's interfaces are unique and its usage is different and not exactly same as other languages. Here are some rules I learned along the way:

Accept interfaces, return struct

In other languages, particularly Java, developers first define an interface, then code the implementation of the interface, then they return and use this interface in all the clients as the default rule. Why? Let's look at the below example.

// foo.java
// define the class
public class Foo {
    void bounce();
}

// client.java
public class Client {
    public void Work(Foo foo) {
        // ...
        // foo.bounce()
    }
}

If the client, wants function Work() to accept any type that can bounce(), it has a problem. The client basically would want to have an interface accepted in Work(). Then to satisfy the interface, it would want to ask the Foo library maintainer to return an interface instead, which might not be possible. Then the client might make a wrapper over Foo to get past this. To avoid these problems firsthand, people first define the interface, implements it, and then ask their clients to use these interfaces, so the rule here is - define interfaces, implement interfaces, and use interfaces!

// foo.java
// define the interface
public interface Foo {
    void bounce();
}

// implement the interface
class FooImpl implements Foo {
}

// client.java
public class Client {
    // use interfaces in client
    public void Work(Foo foo) {
        // ...
        // foo.bounce()
    }
}
But in Go, the interfaces are implicit. Let's revisit the same problem in Go:
// foo.go
package foo

type Foo struct {
}
func New() *Foo {
    return Foo{}
}
func (f *Foo) Bounce() {   
}
                 
// client.go
package client

func Work(f foo.Foo) {
    // ...
    f.Bounce()
}

The function Work()is tightly coupled with the Foo struct of the foo package. If I want to make Work accept any type that can bounce, I do not need to necessarily declare an interface at the service side i.e. at the foo package side, or expect the server side to implement the interface I want. We can declare a client-side interface in Go, check Bouncer interface below. This because interfaces are implicit. This makes Go interfaces stand out. This is a good blog to understand how it is implict. Let's see how this code evolves with client-side interface:

// foo.go
package foo

type Foo struct {
}
func New() *Foo {
    return Foo{}
}
func (f *Foo) Bounce() {   
}
                    
// client.go
package client

// Bouncer is the client side interface
type Bouncer interface {
    Bounce()
}

func Work(f Bouncer) {
    // ...
    f.Bounce()
}

We returned a struct with New() and accepted an client-side declared interface with Work(). Problem solved! Super clean right? The interfaces should be defined on the client side as a default rule. Let the client define the abstaction it needs. The benefits of client side interface is the client can choose to make any combination of functions to make an interface and client has full flexibility on how it wants to use your package. So the philosphy is:

"Never create an interface until you need it, stick to structs! This goes a long way!"

func New() *Foo {}
type Bouncer interface {}
func Work(f Bouncer) {}

Keep interfaces small

In some packages, you would find the interface growing big like this. I have seen this usually happens when people from Java background start adopting Go, one of the reason why it happens was mentioned above.

type UserService interface {
    GetUser(id int) (*User, error)
    CreateUser(email string) (*User, error)
    UpdateUser(email string) (*User, error)
    DestroyUser(id int) error
    GetUserByEmail(email string) (*User, error)
    GetUsers(ids []int) ([]*User, error)
    // ... methods and more methods
}

It is said to keep interfaces small because when the abstraction is small it is more usable. It is more powerful and easy for the clients to use a smaller interface. Mock fewer functions in tests, replace implementations easily, etc..

"Clients should not be forced to depend on methods they do not use." –Robert C. Martin

By default as said in the first rule, we should leave it up to the client to define the interface. What should be the default number of functions that should make an interface? The default should be one, particularly when defining it at your package and not at the client side. Go standard library has many single function interfaces:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Compose Interfaces

For the above example of UserService, we can make smaller interfaces and compose the interfaces to make the interface we need on client side. This is also known as Embedding.

"Build smaller parts to compose a bigger whole."

// user.go
type Getter interface {
    GetUser(id int) (*User, error)
}
type Creater interface {
    CreateUser(email string) (*User, error)
}
type Destroyer interface {
    DestroyUser(id int) error
}

// client.go
import user "..."

// composed interface from small ones
type UserService interface {
    user.Getter
    user.Creater
    user.Destroyer
}

Name interfaces with suffix -er

Interfaces define behavior, the name of the interface should show that. Please avoid prefixing interfaces with I, it does not help, let's name it with the functionality it wraps. I had raised a Reddit question for the same, here. The conclusion from the head banging around the name is, the name should define behavior and the most accepted way is to suffix the interface with `-er`.

"By convention, one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun: Reader, Writer, CloseNotifier etc." - Effective Go

This might work very well if one function makes an interface but what if multiple functions are making the interface? Let's leave the client to construct an interface and name it based on how it is composing it and using it.

In this blog, we answered:

These are the top rules that come to my mind when I review Go interfaces. Do share the ones that you follow and why?

References