feat: initial Go patterns guide from stdlib + Kubernetes source study

9 pattern files covering stdlib (structs, interfaces, API conventions, docs, style),
Kubernetes (controller/reconciler, informer/cache, leader election, code generation),
comparison (stdlib vs K8s approaches), and anti-patterns.

All patterns cite exact source files and line numbers.
This commit is contained in:
Rodin
2026-04-30 06:34:02 +00:00
commit 0f1d7e4c06
9 changed files with 3335 additions and 0 deletions
+390
View File
@@ -0,0 +1,390 @@
# 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`.
### 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).
### 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.
### 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.
### 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` |