golangtutorialAll tutorials

Interfaces in Go: Small Contracts That Improve Design

Learn Go interfaces, implicit satisfaction, consumer-owned contracts, nil traps, composition, and test doubles.

Fundamentals · Lesson 2Saved in this browser. No account required.

An interface is a set of method signatures. A type satisfies it implicitly by implementing those methods; there is no declaration tying the type to the interface.

Define the contract where it is consumed

type UserFinder interface {
	Find(context.Context, int) (User, error)
}

type UserService struct {
	users UserFinder
}

The service needs one behavior, so its interface describes one behavior. The database package can expose a concrete repository without knowing about this interface.

Keep interfaces small

Small interfaces are easier to implement, compose, and test. Do not mirror every method of a concrete type merely to “make it abstract.” Start concrete and extract an interface when a consumer needs substitution.

Use compile-time checks when helpful

var _ UserFinder = (*PostgresUsers)(nil)

This documents the intended relationship and fails at compile time if the implementation drifts.

Understand the typed-nil trap

An interface value contains a dynamic type and dynamic value. An interface holding a nil pointer is not itself nil:

var store *PostgresUsers
var finder UserFinder = store
fmt.Println(finder == nil) // false

Return a literal nil interface when no error or value exists; avoid placing typed nil pointers inside interfaces.

Test with small fakes

type fakeFinder struct {
	user User
	err  error
}

func (f fakeFinder) Find(context.Context, int) (User, error) {
	return f.user, f.err
}

You often do not need a mocking framework. A tiny fake makes test inputs and outputs explicit.

Interfaces are best at real boundaries: storage, clocks, external clients, and other replaceable behavior. They are not a requirement for every struct.