Go interfaces
Go interfaces are satisfied implicitly. The implementing type never names the interface, which flips where the interface belongs.
The consumer declares the interface
Define it in the package that uses it, listing only what that package calls.
// package notify — the consumer. It needs exactly one method.
type UserFinder interface {
FindUser(ctx context.Context, id string) (*User, error)
}
func Send(ctx context.Context, f UserFinder, id string) error { ... }
// package store — the producer. Returns a concrete type, declares no interface.
func New(db *sql.DB) *Store { ... }
func (s *Store) FindUser(ctx context.Context, id string) (*User, error) { ... }
*store.Store satisfies notify.UserFinder with no import between them and no declaration linking them. Producer-side interfaces (store.StoreInterface next to store.Store) invert this: they force every consumer to depend on a surface far wider than it uses, and they change whenever any consumer needs something new.
Keep them small
One or two methods. The standard library's most reused interfaces have one:
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }
A large interface is not reusable, not implementable in a test without a pile of stubs, and usually a struct wearing a disguise. If yours has seven methods, you have described a type, not a capability.
Accept interfaces, return structs
// Good — flexible in, concrete out
func NewProcessor(r io.Reader) *Processor
func (p *Processor) Result() *Report
Returning an interface hides the concrete type's other methods and fields from callers for no gain, and makes the returned value harder to extend without breaking the interface. Return the struct; let the caller narrow it.
Reuse the standard interfaces
Before defining anything, check whether io.Reader, io.Writer, io.Closer, fmt.Stringer, error, sort.Interface, or context.Context already says it. A function taking io.Reader works with files, network connections, strings.Reader, bytes.Buffer, and gzip streams for free. One taking *os.File works with files.
Wait for the second implementation
An interface with one implementor and no test double is indirection with no seam. Write the concrete type, use it, and extract the interface when the second caller or the first test genuinely needs it — extraction is a two-minute refactor, and by then you know which methods belong.
The exception that earns its keep early: a boundary you cannot run in a test (network, clock, filesystem, payment provider). There, the test double is the second implementation.
Testing without a mocking framework
A hand-written fake is usually shorter than the generated mock and reads better in the failure.
type fakeFinder struct {
user *User
err error
}
func (f fakeFinder) FindUser(context.Context, string) (*User, error) {
return f.user, f.err
}
For a one-method interface, a function type removes even that:
type FinderFunc func(context.Context, string) (*User, error)
func (f FinderFunc) FindUser(ctx context.Context, id string) (*User, error) {
return f(ctx, id)
}
This is how http.HandlerFunc works.
Naming
Single-method interfaces take the method name plus -er: Reader, Formatter, UserFinder. No I prefix, no Impl suffix on the implementation. The concrete type gets the plain noun (Store), the interface gets the capability (UserFinder).
Assert satisfaction at compile time
When a type must satisfy an interface it does not mention, state it once so the failure lands at build time with a clear message:
var _ http.Handler = (*Router)(nil)
Empty interfaces and generics
any discards all type information and pushes the failure to runtime. If the function is genuinely type-independent, use a type parameter instead:
// Good
func Keys[K comparable, V any](m map[K]V) []K
// Bad — caller must type-assert, compiler cannot help
func Keys(m any) []any
Reach for generics when the alternative is the same function copy-pasted per type. Do not parameterise a function that has exactly one instantiation.