Files
go-patterns/patterns/interfaces.md
T
aweiker eb9171368b docs: add 'when to use' triggers + examples to all patterns
Added 'When to Use' subsections with concrete decision triggers and
before/after Go code examples to patterns across all directories:

- patterns/error-handling.md (3 patterns: sentinels, wrapping, Join)
- patterns/concurrency.md (4 patterns: Mutex, Once, done channels, pipelines)
- patterns/interfaces.md (4 patterns: small interfaces, accept/return, adapter, optional)
- patterns/structs.md (3 patterns: zero-value, constructors, config structs)
- patterns/package-design.md (3 patterns: internal/, init(), context keys)
- patterns/style.md (3 patterns: interface checks, iota constants, named types)
- patterns/testing-advanced.md (3 patterns: table tests, golden files, httptest)
- patterns/api-conventions.md (3 patterns: Must, layered API, graceful shutdown)
- patterns/documentation.md (2 patterns: examples, deprecated)
- kubernetes/patterns.md (3 patterns: controller, workqueue, leader election)
- kubernetes/production-go.md (2 patterns: codegen, HandleCrash)
- smells/anti-patterns.md (2 anti-patterns: cache mutation, edge-triggered)
2026-04-30 12:08:41 +00:00

526 lines
14 KiB
Markdown

# Go Interface Design Patterns
Patterns extracted from the Go standard library source code.
---
## 1. Small Interfaces (1-2 Methods)
### Source: `src/io/io.go:80-92` (Reader), `93-103` (Writer), `105-109` (Closer)
Go's most powerful interfaces have exactly **one method**:
```go
// src/io/io.go:80-92
type Reader interface {
Read(p []byte) (n int, err error)
}
// src/io/io.go:93-103
type Writer interface {
Write(p []byte) (n int, err error)
}
// src/io/io.go:105-109
type Closer interface {
Close() error
}
```
### Why
Small interfaces are easy to implement and easy to compose. Any type can satisfy `io.Reader` by implementing a single method. This maximizes the number of types that can participate in the ecosystem — files, network connections, buffers, compressors, encryptors all satisfy `Reader`.
### When to Use
**Triggers:**
- You're defining a function that only needs ONE capability from its argument (reading, writing, closing)
- You want maximum reusability — many different types should be able to satisfy your requirement
- You're tempted to create a big interface but realize most consumers only use 1-2 methods
**Example — before:**
```go
// Accepts only *os.File — can't use with buffers, HTTP bodies, test mocks
func countLines(f *os.File) (int, error) {
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
**Example — after:**
```go
// Accepts io.Reader — works with files, HTTP bodies, strings.NewReader, gzip.Reader, etc.
func countLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
### Anti-pattern
```go
// DON'T: Giant interface that tries to cover everything
type FileSystem interface {
Open(name string) (File, error)
Create(name string) (File, error)
Remove(name string) error
Stat(name string) (FileInfo, error)
ReadDir(name string) ([]DirEntry, error)
MkdirAll(path string, perm FileMode) error
// ... 10 more methods
}
```
Large interfaces are hard to implement, hard to mock, and couple consumers to capabilities they don't need.
---
## 2. Interface Composition
### Source: `src/io/io.go:131-155`
Compose small interfaces into larger ones only when needed:
```go
// src/io/io.go:131-134
type ReadWriter interface {
Reader
Writer
}
// src/io/io.go:136-139
type ReadCloser interface {
Reader
Closer
}
// src/io/io.go:141-144
type WriteCloser interface {
Writer
Closer
}
// src/io/io.go:146-150
type ReadWriteCloser interface {
Reader
Writer
Closer
}
```
### Why
Composition lets you express precisely what you need. A function that only reads should accept `Reader`, not `ReadWriteCloser`. Callers provide the minimum; composers build up.
### Anti-pattern
```go
// DON'T: Define a big interface, then use only part of it
func processData(rw ReadWriteCloser) {
// only calls Read... why require Write and Close?
}
```
---
## 3. Accept Interfaces, Return Structs
### Source: `src/io/io.go:461` (LimitReader), `src/io/io.go:618` (TeeReader)
```go
// src/io/io.go:461
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// src/io/io.go:467-471
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
```
```go
// src/io/io.go:618-620
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}
```
### Why
- **Accept interfaces**: Maximizes what callers can pass in — any `Reader` works.
- **Return structs** (or concrete types): Gives callers access to the full type, including fields and methods not in any interface. `LimitedReader` exposes `R` and `N` publicly so callers can inspect remaining bytes.
The return type of `LimitReader` is `Reader` (interface), but the underlying value is `*LimitedReader` (struct). Functions like `io.Copy` can type-assert to `*LimitedReader` to optimize buffer sizes (line 425).
### When to Use
**Triggers:**
- You're writing a function/constructor that operates on a capability (reading, hashing, connecting)
- Your return type has useful fields or methods beyond the interface contract
- You want callers to pass anything that satisfies the contract, but return something concrete they can inspect
**Example — before:**
```go
// Too restrictive input, too vague output
func NewLogger(f *os.File) io.Writer {
return &logger{out: f, level: "info"} // hides SetLevel, Flush methods
}
```
**Example — after:**
```go
type Logger struct {
out io.Writer
level string
}
func (l *Logger) SetLevel(lvl string) { l.level = lvl }
func (l *Logger) Flush() error { /* ... */ }
// Accept interface (any io.Writer), return struct (full access)
func NewLogger(w io.Writer) *Logger {
return &Logger{out: w, level: "info"}
}
```
### Anti-pattern
```go
// DON'T: Accept concrete types
func LimitReader(r *os.File, n int64) Reader // too restrictive
// DON'T: Return interfaces when you have useful struct fields
func NewServer() ServerInterface // hides useful config fields
```
---
## 4. Interface Satisfaction as a Compile-Time Check
### Source: `src/io/io.go:645`, `src/net/http/server.go:4071`
```go
// src/io/io.go:645
var _ ReaderFrom = discard{}
// src/net/http/server.go:4071
var _ Pusher = (*timeoutWriter)(nil)
```
### Why
These blank-identifier declarations cause a compile error if the type stops implementing the interface. They're documentation and safety net combined — no runtime cost.
### Anti-pattern
```go
// DON'T: Discover interface failures at runtime
func doSomething(w ResponseWriter) {
pusher := w.(Pusher) // panics if w doesn't implement Pusher
}
```
---
## 5. Interface-Based Polymorphism (sort.Interface)
### Source: `src/sort/sort.go:16-41`
```go
// src/sort/sort.go:16-41
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
```
### Why
The sort package defines *behavior* it needs, not data it operates on. Any collection — slices, linked lists, database result sets — can be sorted by implementing three methods. The algorithm is decoupled from the data structure.
### Anti-pattern
```go
// DON'T: Hardcode to a specific data type
func Sort(data []int) // only works for []int
func Sort(data []interface{}) // loses type safety
```
Note: Since Go 1.21, `slices.SortFunc` is preferred for slices (generic + faster), but `sort.Interface` remains the pattern for non-slice collections.
---
## 6. The Adapter Pattern (HandlerFunc)
### Source: `src/net/http/server.go:2334-2342`
```go
// src/net/http/server.go:2334-2338
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// src/net/http/server.go:2341-2342
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
```
### Why
This bridges functions and interfaces. Any function with the right signature becomes a `Handler` via `HandlerFunc(myFunc)`. You get the simplicity of functions with the composability of interfaces.
### When to Use
**Triggers:**
- You have an interface with a single method and users frequently implement it with a bare function
- You want to accept both struct-based and function-based implementations of the same behavior
- Requiring a struct definition for simple cases feels like boilerplate
**Example — before:**
```go
type Processor interface {
Process(data []byte) error
}
// User must create a whole struct just to use a function
type upperProcessor struct{}
func (u upperProcessor) Process(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}
```
**Example — after:**
```go
type Processor interface {
Process(data []byte) error
}
// Adapter: any function with the right signature becomes a Processor
type ProcessorFunc func([]byte) error
func (f ProcessorFunc) Process(data []byte) error { return f(data) }
// Now users can write:
pipeline.Use(ProcessorFunc(func(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}))
```
### Anti-pattern
```go
// DON'T: Force users to create a struct just to implement an interface
type myHandler struct{}
func (h myHandler) ServeHTTP(w ResponseWriter, r *Request) {
// just calls a function anyway
handleHome(w, r)
}
```
---
## 7. Optional Interfaces (Runtime Feature Detection)
### Source: `src/net/http/server.go:165-175` (Flusher), `src/net/http/server.go:183-206` (Hijacker)
```go
// src/net/http/server.go:165-170
type Flusher interface {
Flush()
}
// src/net/http/server.go:183-206
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
```
Usage pattern (from doc comments):
```go
// Handlers should always test for this ability at runtime.
if flusher, ok := w.(Flusher); ok {
flusher.Flush()
}
```
### Why
Not every `ResponseWriter` supports flushing or hijacking (HTTP/2 doesn't support Hijacker). Instead of bloating the main interface, optional capabilities are separate interfaces checked at runtime. This keeps the core interface small while allowing progressive enhancement.
### When to Use
**Triggers:**
- Some implementations support a capability but others don't (flushing, hijacking, seeking)
- You want to keep the core interface small but allow optimizations when available
- You're writing middleware that should enhance behavior when possible, not require it
**Example — before:**
```go
// Forces ALL stores to implement caching, even simple ones
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
InvalidateCache() error // not all stores have a cache!
}
```
**Example — after:**
```go
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
}
// Optional capability — check at runtime
type Cacheable interface {
InvalidateCache() error
}
func refreshAll(s Store) {
if c, ok := s.(Cacheable); ok {
c.InvalidateCache() // only if supported
}
}
```
### Anti-pattern
```go
// DON'T: Put optional capabilities in the main interface
type ResponseWriter interface {
Write([]byte) (int, error)
WriteHeader(int)
Header() Header
Flush() // not always supported!
Hijack() (net.Conn, *bufio.ReadWriter, error) // not always supported!
}
```
---
## 8. The Stringer Interface (Convention-Based Behavior)
### Source: `src/fmt/print.go:63-66`
```go
// src/fmt/print.go:63-66
type Stringer interface {
String() string
}
```
### Why
`fmt` checks if a value implements `Stringer` and calls `String()` for its text representation. This is a *convention*: any type in any package can participate without importing `fmt`. The interface acts as a protocol.
Similarly, `GoStringer` (line 71) controls `%#v` output, and `Formatter` (line 58-61) gives total control over formatting.
### Anti-pattern
```go
// DON'T: Use type switches over known types
func printThing(v any) string {
switch v := v.(type) {
case MyType: return v.name
case OtherType: return v.label
// ... must know about every type
}
}
```
---
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
### Source: `src/io/io.go:410-417`
```go
// src/io/io.go:410-417
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rf, ok := dst.(ReaderFrom); ok {
return rf.ReadFrom(src)
}
// ... fallback to buffer-based copy
}
```
### Why
The core function works with the minimum interface (`Reader`/`Writer`) but *upgrades* behavior when richer interfaces are available. `*os.File` implements `ReadFrom` using `sendfile(2)` — zero-copy kernel transfer. The generic path works, but specialized implementations optimize transparently.
### Anti-pattern
```go
// DON'T: Always use the slow generic path
func Copy(dst Writer, src Reader) {
buf := make([]byte, 32*1024)
// always allocates, never leverages kernel optimizations
}
```
---
## 10. The driver.Driver Pattern (Plugin Interfaces)
### Source: `src/database/sql/driver/driver.go:85-97`, `104-112`
```go
// src/database/sql/driver/driver.go:85-97
type Driver interface {
Open(name string) (Conn, error)
}
// src/database/sql/driver/driver.go:104-112
type DriverContext interface {
OpenConnector(name string) (Connector, error)
}
```
### Why
The `database/sql` package defines interfaces that driver authors implement. The base interface (`Driver`) is minimal (one method). Extended capabilities (`DriverContext`, `Pinger`, `Execer`, `Queryer`) are opt-in — the `sql` package checks for them at runtime. This allows the ecosystem to grow without breaking existing drivers.
### Anti-pattern
```go
// DON'T: One massive interface all drivers must implement
type Driver interface {
Open(name string) (Conn, error)
OpenConnector(name string) (Connector, error)
Ping(ctx context.Context) error
// ... every driver must implement everything, even if not supported
}
```
---
## Summary: Go Interface Design Principles
| Principle | Standard Library Example |
|-----------|------------------------|
| Keep interfaces small (1-2 methods) | `io.Reader`, `io.Writer`, `fmt.Stringer` |
| Compose small interfaces | `io.ReadWriteCloser` |
| Accept interfaces, return structs | `io.LimitReader`, `io.TeeReader` |
| Use adapter types for functions | `http.HandlerFunc` |
| Optional capabilities via separate interfaces | `http.Flusher`, `http.Hijacker` |
| Compile-time interface checks | `var _ Interface = (*Type)(nil)` |
| Runtime interface upgrade for optimization | `io.Copy``WriterTo`/`ReaderFrom` |
| Plugin/driver interfaces start minimal | `database/sql/driver.Driver` |