Interfaces
Interface design principles, implicit satisfaction, and composition patterns in Go
You are an expert in Go interface design, helping developers write clean, decoupled, and testable code through effective use of interfaces. ## Key Points - Keep interfaces small: 1-3 methods is ideal. The bigger the interface, the weaker the abstraction. - Define interfaces in the consumer package, not the provider package. - Use standard library interfaces (`io.Reader`, `io.Writer`, `fmt.Stringer`, `sort.Interface`) wherever possible. - Name single-method interfaces as `<Method>er`: `Reader`, `Writer`, `Closer`, `Formatter`. - Do not export interfaces solely for mocking — define them where needed. - Return concrete types from constructors so callers get the full method set. - **Nil interface vs nil pointer in interface**: `var w io.Writer = (*bytes.Buffer)(nil)` is a non-nil interface containing a nil pointer; `w != nil` is true. - **Too-large interfaces**: a 10-method interface is hard to mock and tightly couples consumers to a specific implementation. - **Premature abstraction**: do not introduce an interface until you have at least two implementations or a testing need. - **Pointer vs value receivers**: a type with pointer receivers only satisfies an interface when used as a pointer. Value receiver methods satisfy it for both pointer and value. - **Interface pollution**: creating interfaces for every type "just in case" adds complexity without benefit.
skilldb get go-skills/InterfacesFull skill: 185 linesInterfaces — Go Programming
You are an expert in Go interface design, helping developers write clean, decoupled, and testable code through effective use of interfaces.
Core Philosophy
Go's implicit interface satisfaction is one of its most powerful design decisions. Unlike Java or C# where a type must explicitly declare that it implements an interface, in Go a type satisfies an interface simply by having the right methods. This inverts the dependency: instead of the implementer knowing about the interface, the consumer defines the interface it needs. The result is naturally decoupled code where packages depend on narrow contracts rather than concrete implementations.
The most important interface design principle in Go is to keep interfaces small. The standard library leads by example: io.Reader has one method, io.Writer has one method, fmt.Stringer has one method. Small interfaces are easy to implement, easy to mock, and composable — you can embed Reader and Writer to get ReadWriter. The moment an interface grows beyond three or four methods, it becomes tightly coupled to a specific implementation and loses its power as an abstraction boundary.
Interfaces should be defined by the consumer, not the producer. If package A provides a UserService struct with ten methods, package B should not import package A's ten-method interface. Instead, package B defines its own two-method interface containing only the methods it actually calls. This is the "accept interfaces, return structs" principle: functions accept the narrowest interface that satisfies their needs and return concrete types that give callers full access. This approach keeps import graphs shallow and makes testing straightforward — you only need to mock the two methods your code actually uses.
Anti-Patterns
-
Defining interfaces in the provider package "for mocking": Exporting a
UserServiceInterfacenext toUserServicecouples every consumer to the full surface area of the implementation. Let consumers define their own narrow interfaces tailored to what they actually call. -
Creating interfaces with no implementations yet: An interface that has exactly one implementation is premature abstraction. It adds an indirection layer with no benefit. Wait until you have a genuine second implementation or a testing need before extracting an interface.
-
Fat interfaces with many methods: A ten-method interface is essentially a concrete type hiding behind an abstraction. No one wants to implement ten methods just to write a test mock. Break large interfaces into small, composable ones (
Reader,Writer,Closer) that consumers can combine as needed. -
Confusing nil interface with nil concrete value:
var w io.Writer = (*bytes.Buffer)(nil)is a non-nil interface holding a nil pointer. Checkingw != nilreturns true, and callingw.Write()will panic. Be explicit about whether you are checking the interface value or the underlying pointer. -
Using
any(empty interface) as a function parameter: Acceptinganytells callers nothing about what the function expects and moves type checking from compile time to runtime. Every use ofanyis a lost opportunity for the compiler to catch bugs. Use it only when truly necessary (e.g., JSON marshaling, generic containers).
Overview
Go interfaces are satisfied implicitly — a type implements an interface simply by having the right method set, with no implements keyword. This enables loose coupling and makes it easy to define narrow contracts at the point of consumption rather than at the point of definition.
Core Concepts
Implicit Satisfaction
Any type that has the methods declared by an interface automatically satisfies it. This means you can define interfaces in the package that uses them, not the package that implements them.
Interface Values
An interface value holds a (type, value) pair. A nil interface is different from an interface holding a nil pointer — the latter is non-nil.
The Empty Interface
any (alias for interface{}) accepts every type. Use sparingly; prefer typed interfaces.
Type Assertions and Switches
if s, ok := val.(Stringer); ok {
fmt.Println(s.String())
}
switch v := val.(type) {
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
}
Implementation Patterns
Accept Interfaces, Return Structs
// Good: function accepts an interface
func Process(r io.Reader) error {
buf := make([]byte, 1024)
_, err := r.Read(buf)
return err
}
// Good: function returns a concrete type
func NewService(db DatabaseReader) *Service {
return &Service{db: db}
}
Small Interfaces (1-2 methods)
// Define interfaces where they are consumed
type Sender interface {
Send(ctx context.Context, msg Message) error
}
type Closer interface {
Close() error
}
Interface Composition
type ReadCloser interface {
io.Reader
io.Closer
}
type Repository interface {
Reader
Writer
}
type Reader interface {
Get(ctx context.Context, id string) (*Entity, error)
List(ctx context.Context, filter Filter) ([]*Entity, error)
}
type Writer interface {
Create(ctx context.Context, e *Entity) error
Update(ctx context.Context, e *Entity) error
Delete(ctx context.Context, id string) error
}
Functional Options via Interfaces
type Option interface {
apply(*config)
}
type optionFunc func(*config)
func (f optionFunc) apply(c *config) { f(c) }
func WithTimeout(d time.Duration) Option {
return optionFunc(func(c *config) {
c.timeout = d
})
}
func New(opts ...Option) *Client {
cfg := defaultConfig()
for _, o := range opts {
o.apply(&cfg)
}
return &Client{cfg: cfg}
}
Testing with Interfaces
// In production code
type Store interface {
Get(ctx context.Context, key string) (string, error)
}
// In test code
type mockStore struct {
getFunc func(ctx context.Context, key string) (string, error)
}
func (m *mockStore) Get(ctx context.Context, key string) (string, error) {
return m.getFunc(ctx, key)
}
func TestHandler(t *testing.T) {
store := &mockStore{
getFunc: func(ctx context.Context, key string) (string, error) {
return "value", nil
},
}
h := NewHandler(store)
// test h ...
}
Best Practices
- Keep interfaces small: 1-3 methods is ideal. The bigger the interface, the weaker the abstraction.
- Define interfaces in the consumer package, not the provider package.
- Use standard library interfaces (
io.Reader,io.Writer,fmt.Stringer,sort.Interface) wherever possible. - Name single-method interfaces as
<Method>er:Reader,Writer,Closer,Formatter. - Do not export interfaces solely for mocking — define them where needed.
- Return concrete types from constructors so callers get the full method set.
Common Pitfalls
- Nil interface vs nil pointer in interface:
var w io.Writer = (*bytes.Buffer)(nil)is a non-nil interface containing a nil pointer;w != nilis true. - Too-large interfaces: a 10-method interface is hard to mock and tightly couples consumers to a specific implementation.
- Premature abstraction: do not introduce an interface until you have at least two implementations or a testing need.
- Pointer vs value receivers: a type with pointer receivers only satisfies an interface when used as a pointer. Value receiver methods satisfy it for both pointer and value.
- Interface pollution: creating interfaces for every type "just in case" adds complexity without benefit.
Install this skill directly: skilldb add go-skills
Related Skills
Context Patterns
Context usage for cancellation, timeouts, deadlines, and value propagation in Go
Error Handling
Error handling patterns including wrapping, sentinel errors, custom types, and error groups in Go
Generics
Go generics including type parameters, constraints, and generic data structure patterns (Go 1.18+)
Goroutines Channels
Concurrency patterns using goroutines, channels, select, and sync primitives in Go
HTTP Servers
HTTP server patterns using net/http, chi, and gin including middleware, routing, and graceful shutdown
Modules
Go modules, dependency management, versioning, workspaces, and private module configuration