Files
go-patterns/patterns/interfaces.md
T
Rodin 0f1d7e4c06 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.
2026-04-30 06:34:02 +00:00

10 KiB

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:

// 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

// 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:

// 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

// 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)

// 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
}
// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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)

// 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):

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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.CopyWriterTo/ReaderFrom
Plugin/driver interfaces start minimal database/sql/driver.Driver