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.