Defining and Implementing Interfaces
Go interfaces describe behavior through method sets satisfied implicitly by concrete types.
Define small interfaces at the consumer, accept them in function parameters, and return concrete structs from constructors.
Summary
An interface value holds a dynamic type and dynamic value pair.
Satisfaction is compile-time checked when you assign; no implements keyword exists.
Small interfaces (often one or two methods) keep mocks thin and encourage composition over wide abstractions.
Accept interfaces, return structs is the headline API rule: depend on minimal behavior in parameters; expose concrete types from New functions so callers are not trapped behind your interface definitions.
Avoid exporting large interfaces from producer packages - they become painful compatibility contracts.
Recipe
Quick-reference recipe card - copy-paste ready.
// Consumer package defines what it needs.
type Storage interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, value []byte) error
}
// Producer returns concrete type.
type MemStore struct {
mu sync.RWMutex
m map[string][]byte
}
func NewMemStore() *MemStore {
return &MemStore{m: make(map[string][]byte)}
}
func (s *MemStore) Get(ctx context.Context, key string) ([]byte, error) { /* ... */ }
func (s *MemStore) Put(ctx context.Context, key string, value []byte) error { /* ... */ }
func LoadConfig(ctx context.Context, store Storage, key string) ([]byte, error) {
return store.Get(ctx, key)
}When to reach for this:
- Swapping implementations in tests (memory vs fake vs integration).
- Libraries must not import concrete cloud SDKs at call sites.
- Multiple types share overlapping behavior (
io.Reader,io.Writer). - You define extension points in frameworks (HTTP
Handler, gRPCServerStream).
Working Example
package notify
import (
"context"
"fmt"
)
type Sender interface {
Send(ctx context.Context, to, body string) error
}
type Service struct {
sender Sender
}
func New(sender Sender) *Service {
return &Service{sender: sender}
}
type LogSender struct{}
func (LogSender) Send(ctx context.Context, to, body string) error {
fmt.Printf("to=%s body=%q\n", to, body)
return nil
}
type SMTPClient struct {
host string
}
func NewSMTP(host string) *SMTPClient {
return &SMTPClient{host: host}
}
func (c *SMTPClient) Send(ctx context.Context, to, body string) error {
// dial c.host, send message ...
return nil
}
func (s *Service) Welcome(ctx context.Context, email string) error {
return s.sender.Send(ctx, email, "welcome")
}What this demonstrates:
Senderinterface lives with consumerService, not SMTP package.- Two concrete senders satisfy the same interface without explicit declaration.
NewacceptsSenderfor injection; returns*Serviceconcrete type.- Context threads through interface methods for cancellation/timeouts.
Deep Dive
How It Works
- Interface type with method set
Mis satisfied by any type whose method set includesM. - Empty interface
anyholds all types; use only at decoding boundaries. - Non-interface nil is untyped nil; interface nil requires both type and value slots empty.
- Embedding interfaces promotes methods - useful for
io.ReadCloserstyle compositions.
Interface Design Rules
| Rule | Rationale |
|---|---|
| Define at consumer | Producers stay independent |
| Keep 1-3 methods | Small fakes, easier evolution |
| Name by capability | Storage, Sender, not IStorage |
Return structs from New | Callers not forced to import interface types |
| Document nil behavior | Especially for returned interfaces |
Standard Library Patterns
| Interface | Methods | Typical implementers |
|---|---|---|
io.Reader | Read | Files, buffers, network |
fmt.Stringer | String | Domain types for logging |
error | Error | Sentinels, wrapped errors |
http.Handler | ServeHTTP | Routers, chi/gin/echo handlers |
Go Notes
// Split interfaces when methods are unrelated.
type Reader interface { Read(p []byte) (int, error) }
type Writer interface { Write(p []byte) (int, error) }
type ReadWriter interface {
Reader
Writer
}Prefer embedding to bloated single interfaces.
Gotchas
- Exported mega-interfaces - Adding methods breaks all implementers. Fix: split interfaces or keep unexported interfaces satisfied by exported structs.
- Returning interface from constructor - Hides concrete type, encourages typed nil bugs. Fix: return
*Concreteunless abstraction is the product. - Pointer vs value satisfaction - Value missing pointer-only methods fails assignment. Fix: return pointers from constructors when needed.
- Comparing interface to nil incorrectly - Typed nil pointer inside interface is not
== nil. Fix: return concrete types or document checks with reflection/errors.Ispatterns. - Duplicate method names across embedded interfaces - Ambiguous promotions fail compile. Fix: rename methods or embed one interface only.
- Interface pollution in hot paths - Minor allocation/itable cost usually fine; profile before removing interfaces. Fix: optimize only with evidence.
- Testing with testify/mock on wide interfaces - Brittle expectations. Fix: narrow interfaces or hand-written fakes.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Function parameter | Single hook (Validator func() error) | Multiple coordinated methods |
| Generics constraint | Shared algorithms over types | Runtime plugin swapping |
| Concrete type only | Single implementation forever | Need tests with fakes |
any + type switch | JSON plugin registry | Stable domain APIs |
FAQs
How does a type implement an interface?
Declare methods with matching names and signatures.
The compiler verifies at assignment - no explicit declaration.
Where should interfaces be defined?
At the package that uses the behavior, not the package that provides implementations.
Exception: standard library idioms like io.Reader define shared contracts.
How small should an interface be?
Often one method (Reader, Writer, Stringer).
Add methods only when callers truly need them together.
Can interfaces have fields?
No - only methods.
Share data through concrete structs passed alongside interfaces or returned values.
What is a nil interface?
Both type and value slots are unset.
Different from an interface containing a typed nil pointer.
Should repositories always be interfaces?
In application packages, yes - for test doubles.
In simple CLIs with one DB, a concrete type may suffice until tests demand fakes.
How do I mock gRPC clients?
Use generated interfaces or narrow wrappers around specific RPC methods.
Avoid mocking entire generated client structs when one method matters.
Can I add methods to an exported interface?
It is a breaking change for external implementers.
Introduce a new interface name or unexported interface inside your package.
How does errors.As relate to interfaces?
errors.As checks whether an error implements an interface and extracts it.
Define sentinel interfaces for classification sparingly.
Should HTTP middleware depend on gin.Context?
Prefer http.Handler interfaces for portability across chi, gin, echo adapters.
Framework types leak when used as domain boundaries.
What about interface { Private() } patterns?
Unexported methods on interfaces restrict implementation to your package.
Useful for sealing implementations while exporting the interface type.
How does golangci-lint guide interfaces?
Linters like iface detect unused interfaces and suggest removal.
ireturn may flag functions returning interfaces - align with team style.
Related
- Functions and Interfaces: Go's Composition Model - why implicit interfaces exist
- Nil Interface vs Nil Pointer - typed nil inside interface values
- Methods: Value vs Pointer Receivers - method sets and satisfaction
- Variadic Functions & Function Types - when a func type beats an interface
- Functions, Methods & Interfaces Best Practices - interface sizing and export rules
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).