eb9171368b
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)
526 lines
14 KiB
Markdown
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` |
|