Image for post
Image for post

Preface

Introduction to Go Interfaces

From the above example, you can see that:

  • A Go interface defines one or more methods.
  • Unlike other languages, you don’t have to explicitly declare that a type implements an interface. A struct S is a thing which is defined by an interface I as long as the struct S implements all the methods defined by the interface I. In this example,FlyWithWings and FlyWithSuperPower are both FlyBehaviour as they all implement the methods defined in the interface FlyBehaviour.

Best Practice (a.k.a. Arguments)

“The smaller the interface,the stronger the abstraction”

The following shows an example that follows this principle, in which the interface user.Manager is split into multiple smaller interfaces.

"Accept interfaces but return structs”

The benefit of returning structs is to give consumers the freedom to define interfaces on their side. Take the above interface user.ManagerInterface as an example, consumers can define this interface based on the struct user.Manager (the implementation of user manager) and their need in their own packages.

The benefit of returning interfaces is to free consumers from defining interfaces on their own, as long as the returning interfaces provide strong abstraction. Moreover, some design patterns, such as Factory Method Pattern, require to return interfaces other than concrete implementation. For example, the method aes.newCipher returns the interface cipher.Block with different structs in different circumstances:

Normally, the smaller the interface,the stronger the abstraction. The stronger the abstraction, the more acceptable it is to return interfaces.

“Go interfaces generally belong in the package that uses values of the interface type, not the package that implements those values”

“If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself. Exporting just the interface makes it clear the value has no interesting behavior beyond what is described in the interface.”

So where should we define interfaces? Consumer side or producer side? I think this depends on whether the interface that you define has good abstraction or not. If an interface provides very strong abstraction, then it makes sense to put it in an individual shared package. The interface io.Reader in the package io is a perfect example of interfaces with strong abstraction. It only has one method Read and everyone agrees what this method should look like. Therefore, everyone is fine with putting the interface Reader in the package io, which is a producer that provides implementations of this interface.

The opposite example of putting Go interfaces on the consumer side is Go client of Github. It is apparently not a good idea to define a github.Client interface as a Github client needs to provide a lot of methods, which leads to weak abstraction for making a github.Client interface. In this case, it makes more sense for consumers to define their own github.Client interfaces, which may vary from consumer to consumer and may only have very few methods.

“It can be useful to define a default abstraction on the producer side”

Suppose the package user is an internal package for a project and it has five consumers (five packages are using this package). The interface user.ManagerInterface needs to be defined five times in these packages for programming to interfaces, if there is no default interface user.ManagerInterface and it is very likely these packages may define the same interface user.ManagerInterface. Therefore, it can be very useful for the package user (producer) to provide a default interface and its mocks in this case. Here is the pseudo-code:

From the above example, you can see that:

  • The implementation user.Manager is exposed and returned, which gives consumers the freedom to define the abstraction of user.ManagerInterface base on this implementation.
  • The package user provides a default abstraction of user.Manager and its mocks through the interface user.ManagerInterface and the struct user.ManagerMock, which frees some consumers from defining the abstraction ofuser.Manager and its mocks.
  • This pattern is a trade-off of the argument about where to define interfaces. That is, provide default abstraction on the producer side but also give consumers the freedom to define their own abstraction. And most importantly, let consumers decide what they want to use.

Summary

Reference

A software engineer

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store