In this article, we’ll look at a very common mistake that Golang new comers make, especially when they come from a background of Object Oriented Programming (OOP) languages like Java or C#.

What is interface pollution?

Let’s look at some codes.

package employee

type Employee struct {
	Name string
	Credit int
}

type EmployeeRepository interface {
	Query(name string) *Employee
}

func NewEmployeeRepository() EmployeeRepository {
	return &database{}
}

type database struct {}

// Query returns the first Employee that has a matching name.
func (d *database) Query(name string) *Employee {
  // ... 
}

We implement an employee package, providing a method to query the storage by employee name, which is exposed through the API of the package: the EmployeeRepository interface. This interface construct abstracts database, the concrete implementation that does the real work. This simply follows good-old OOP design principles that we’re all used to, so what is the problem here? The first thing to note is that the EmployeeRepository interface has only one implementation. In fact, if we remove EmployeeRepository interface, and expose database to the outer world like the following code, the package API does not change.

package employee

type Employee struct {
	Name string
	Credit int
}

type Database struct {}

func NewDatabase() *Database {
	return &Database{}
}

// Query returns the first Employee that has a matching name.
func (d *Database) Query(name string) *Employee {
  // ... 
}

Our package users can still query employee storage by name using the Query method of Database. EmployeeRepository is polluting employee package, and returning *Database is the better design here. In general “accept interface, return struct” is a guideline that any Go developers should keep in mind.

But what about loose coupling?

Without the EmployeeRepository interface, our code is tightly coupled. Look at how package employee is used in the following codes.

// how our users use the package

import employee

func GrantMonthlyCredit(d *employee.Database, name string) {
	e := d.Query(name)
	e.Credit += 10
}

User codes will depend on *employee.Database, which is a concrete implementation rather than an abstration. How do we even write tests for GrantMonthlyCredit?

Well, it’s actually true that GrantMonthlyCredit is tightly coupled with *employee.Database in the codes above, but it’s not because of the design of employee package! The design of interface in Go is brilliant: instead of having to tell explicitly which type implements which interface, any types in Go can implement an interface if it implements the methods required by that interface. Interface implementation in Go is implicit (how awesome that is!). This allows us to come up with robust techniques like constructing an on-the-fly interface, and thus keeping our package design clean. It’s possible to define an interface in user codes that Database satisfies.

import employee

// define an on-the-fly interface in user codes
type employeeRepository interface {
	Query(name string) *employee.Employee
}

// use the on-the-fly interface in GrantMonthlyCredit, 
// making it decoupled from the concrete implementation of *employee.Database
func GrantMonthlyCredit(r employeeRepository, name string) {
	e := r.Query(name)
	e.Credit += 10
}

To test GrantMonthlyCredit, we can create a stub, or use a mocking framework to create a programmable mocked version of employeeRepository. In our production code, we inject the real implementation: the Database we get from employee package.

// employeeRepositoryStub is a stub we can use in our unit tests
type employeeRepositoryStub struct{}

func (s *employeeRepositoryStub) Query(name string) *employee.Employee {
	return &Employee{Name: name, Credit: 5}
}

// in unit tests
r := new(employeeRepositoryStub)
GrantMonthlyCredit(r, "john")

// in production code
d := employee.NewDatabase()
GrantMonthlyCredit(d, "john")

The exception

We learn that “accept interface, return struct” can keep our package design clean. However, it’s perfectly ok to return an interface in our package API if we have multiple implementations for it. Let’s take a look at the standard context package.

func Background() Context { ... }

func TODO() Context { ... }

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { ... }

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { ... }

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { ... }

func WithValue(parent Context, key, val any) Context { ... }

context package provides a few functions that returns Context, an interface. The concrete implementation of this interface depends on the use case that we have. For example, WithValue returns a *valueCtx while WithDeadline returns a *timerCtx. It’s not recommended to poke around the internals of these concrete types, and users are recommended to focus on the contract that Context gives us instead. This encapsulation makes so much sense because the developer of the context package can make aggressive changes, maybe rework how valueCtx works, or maybe to add a new context type, without affecting the user’s codes.

Another exception is when we want to let users provide the implementations for the interfaces that our package exercises. For instance, the io package defines the Reader and Writer interfaces, and a few utilities to work with these interfaces. One use case is to copy a stream of data to another, we can use the io.Copy function with our input stream that implements the io.Reader interface, and our output stream that implements the io.Writer interface. All other utilities of io are designed around these interfaces, making I/O operations in Go extremely powerful.

Summary

Avoid interface pollution. It’s always possible to create an on-the-fly interface out of a package type for decoupling. Only include an interface in your package API if one of these conditions is met:

  • The user of the package will need to provide an implementation that satisfies the interface. A good example is the io.Reader and io.Writer interfaces in io package.
  • The package maintains a few implementations of the interface that it does not wish to expose their internals to the package user (e.g. the context package).