Files
go-patterns/patterns/structs.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

500 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Struct Design Patterns in the Go Standard Library
## 1. Zero-Value Usability
**Pattern name:** Zero Value Ready
**Source citation:** `net/http/client.go` lines 3135, `strings/builder.go` lines 1416
**What it does:** Structs are designed so their zero value is immediately useful without
explicit initialization. Nil fields fall back to sensible defaults at method call time.
**Why:** Eliminates mandatory constructors, reduces boilerplate, makes the type
self-documenting about its defaults. Users can write `var c http.Client` and start
making requests.
**When to Use**
**Triggers:**
- You're designing a type where the "empty" or "default" state is meaningful and safe
- Users should be able to write `var x MyType` and immediately call methods
- Your struct's nil/zero fields can fall back to sensible defaults at call time
**Example — before:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration
}
// Panics on zero value — store is nil!
func (c *Cache) Set(k string, v []byte) { c.store[k] = v }
```
**Example — after:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration // zero means no expiry
}
func (c *Cache) Set(k string, v []byte) {
if c.store == nil {
c.store = make(map[string][]byte) // lazy init on first use
}
c.store[k] = v
}
```
**Anti-pattern:** Requiring a constructor for basic use; panicking on zero-value use;
requiring all fields be set before the type is functional.
**Code examples from source:**
```go
// net/http/client.go:31-35
// A Client is an HTTP client. Its zero value ([DefaultClient]) is a
// usable client that uses [DefaultTransport].
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
// net/http/client.go:109
var DefaultClient = &Client{}
```
```go
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
```
```go
// bytes/buffer.go:19-20
// A Buffer is a variable-sized buffer of bytes with [Buffer.Read] and [Buffer.Write] methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
buf []byte
off int
lastRead readOp
}
```
---
## 2. Unexported Struct with Exported Wrapper
**Pattern name:** Indirection via Unexported Impl
**Source citation:** `os/types.go` lines 1620, `os/file_unix.go` lines 5971
**What it does:** The exported type (`File`) embeds a pointer to an unexported type
(`*file`) that holds the real implementation state. Users interact only with the
exported wrapper.
**Why:** Prevents users from directly constructing or copying the implementation struct.
Allows platform-specific implementations behind a uniform exported API. The extra
indirection ensures finalizers close the correct descriptor.
**Anti-pattern:** Exporting all implementation fields; allowing users to construct
the struct via a literal (bypassing invariants); needing platform #ifdefs in the
public API.
**Code example from source:**
```go
// os/types.go:16-20
// File represents an open file descriptor.
//
// The methods of File are safe for concurrent use.
type File struct {
*file // os specific
}
// os/file_unix.go:59-71
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
pfd poll.FD
name string
dirinfo atomic.Pointer[dirInfo]
nonblock bool
stdoutOrErr bool
appendMode bool
inRoot bool
}
```
---
## 3. Constructor Functions (NewXxx)
**Pattern name:** NewXxx Constructor
**Source citation:** `bufio/scan.go` lines 8996, `bufio/bufio.go` lines 5060
**What it does:** A package-level function `NewXxx(deps) *Xxx` constructs the type
with required dependencies and internal defaults that can't be expressed via zero
value alone.
**Why:** When a type has mandatory dependencies (e.g., an `io.Reader`), a constructor
clearly communicates what's required. The constructor can set internal invariants
(buffer sizes, split functions) that users shouldn't need to know about.
**When to Use**
**Triggers:**
- Your type has mandatory dependencies that can't be expressed as zero values (an `io.Reader`, a DB connection)
- Internal invariants must be set up (buffer allocation, goroutine start)
- The type isn't useful without initialization (unlike `sync.Mutex` or `bytes.Buffer`)
**Example — before:**
```go
type Parser struct {
lexer *Lexer
buf []Token
maxDepth int
}
// User must know about all internal state:
p := &Parser{lexer: NewLexer(input), buf: make([]Token, 0, 64), maxDepth: 100}
```
**Example — after:**
```go
func NewParser(input io.Reader) *Parser {
return &Parser{
lexer: NewLexer(input),
buf: make([]Token, 0, 64),
maxDepth: 100,
}
}
// User writes:
p := NewParser(file)
```
**Anti-pattern:** Forcing users to manually set unexported fields; having a constructor
that takes 10 optional parameters (use config struct instead); requiring New when
zero value would suffice.
**Code examples from source:**
```go
// bufio/scan.go:89-96
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
```
```go
// bufio/bufio.go:50-62
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
r := new(Reader)
r.reset(make([]byte, max(size, minReadBufferSize)), rd)
return r
}
// NewReader returns a new [Reader] whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
```
```go
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
```
---
## 4. NewXxx with Size/Options Variant
**Pattern name:** NewXxx / NewXxxSize Pair
**Source citation:** `bufio/bufio.go` lines 50, 62, 589, 607
**What it does:** Provides two constructors — one with defaults (`NewReader`) and one
with explicit configuration (`NewReaderSize`). The default version calls the
configurable one.
**Why:** Most users want the default; power users need control. Layering avoids a
proliferation of constructor parameters for the common case.
**Anti-pattern:** Having only the complex constructor; making users guess the right
buffer size; inconsistent naming (e.g., `NewReaderWithSize`).
**Code example from source:**
```go
// bufio/bufio.go:589-607
func NewWriterSize(w io.Writer, size int) *Writer {
// ...
}
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
```
---
## 5. Config Struct Pattern
**Pattern name:** Configuration Struct (Exported Fields, Nil-Means-Default)
**Source citation:** `net/http/server.go` lines 30203120, `crypto/tls/common.go` lines 566+, `log/slog/handler.go` lines 135175
**What it does:** A struct with exported, documented fields provides all
configuration knobs. Nil/zero values always mean "use the default".
**Why:** Self-documenting via godoc; no need for a setter method per option; easy to
construct partially; serializable; the zero value works. This is Go's primary
configuration pattern (preferred over functional options in the stdlib).
**When to Use**
**Triggers:**
- Your constructor has 4+ optional parameters that would make a function signature unwieldy
- You want users to see all options in one place with godoc documentation
- Zero/nil values should mean "use the default" — no required fields beyond what the constructor demands
**Example — before:**
```go
// 7 parameters — impossible to remember the order
func NewServer(addr string, handler http.Handler, readTimeout, writeTimeout time.Duration,
maxConns int, logger *log.Logger, tlsConfig *tls.Config) *Server { ... }
```
**Example — after:**
```go
type ServerConfig struct {
Addr string // ":8080" if empty
Handler http.Handler // http.DefaultServeMux if nil
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxConns int // 1000 if zero
Logger *log.Logger // log.Default() if nil
TLSConfig *tls.Config // plain HTTP if nil
}
func NewServer(cfg ServerConfig) *Server { ... }
```
**Anti-pattern:** Undocumented fields; requiring all fields set; using sentinel values
other than zero/nil for defaults; providing setters when direct assignment works.
**Code example from source:**
```go
// net/http/server.go:3020-3075 (abbreviated)
type Server struct {
Addr string // ":http" if empty
Handler Handler // http.DefaultServeMux if nil
TLSConfig *tls.Config // optional
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxHeaderBytes int // DefaultMaxHeaderBytes if zero
ErrorLog *log.Logger // log.Default() if nil
// ...
}
```
```go
// log/slog/handler.go:135-175
type HandlerOptions struct {
AddSource bool
Level Leveler // LevelInfo if nil
ReplaceAttr func(groups []string, a Attr) Attr
}
// Usage: If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
// ...
}
```
---
## 6. Interface-Based Pluggability
**Pattern name:** Interface Abstraction for Pluggable Implementations
**Source citation:** `crypto/crypto.go` lines 180200, `net/http/transport.go` lines 6682
**What it does:** Core behavior is defined via an interface. The package provides
a default concrete implementation, but any user type satisfying the interface
can be substituted.
**Why:** Decouples high-level logic from low-level implementation. Enables testing
(mock transports), hardware integration (HSM-backed signers), and third-party
extensions without forking the package.
**Anti-pattern:** Concrete-type coupling everywhere; interfaces with too many methods
(hard to implement); accepting an interface but only ever using one implementation.
**Code example from source:**
```go
// crypto/crypto.go:180-200
// Signer is an interface for an opaque private key that can be used for
// signing operations. For example, an RSA key kept in a hardware module.
type Signer interface {
Public() PublicKey
Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
```
```go
// net/http/transport.go (line 66+)
// Transport is an implementation of [RoundTripper] that supports HTTP,
// HTTPS, and HTTP proxies...
// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.
// net/http/client.go:59-60
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
```
---
## 7. Copy Protection via Dynamic Check
**Pattern name:** copyCheck (Runtime Copy Detection)
**Source citation:** `strings/builder.go` lines 2540
**What it does:** On first mutation, the Builder records its own address. Subsequent
mutations compare the current receiver address against the recorded one. If they
differ, the struct was copied — it panics.
**Why:** Go has no language-level move semantics. For types where copying after first
use would cause data corruption or unsafe behavior (e.g., sharing an unsafe string
buffer), a runtime check is the pragmatic solution.
**Anti-pattern:** Silently allowing copies that corrupt state; using `sync.Mutex`-style
`noCopy` (vet catches it but it doesn't work for zero vs non-zero discrimination).
**Code example from source:**
```go
// strings/builder.go:25-40
func (b *Builder) copyCheck() {
if b.addr == nil {
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
```
---
## 8. DefaultXxx Singleton
**Pattern name:** Package-Level Default Instance
**Source citation:** `net/http/client.go` line 109, `net/http/transport.go` lines 4758
**What it does:** The package provides a pre-configured, ready-to-use instance as
a package-level variable. Package-level convenience functions delegate to it.
**Why:** Makes the simple case trivial (`http.Get(url)`) while allowing custom
instances for advanced use. Users never need to touch the defaults unless they
have specific requirements.
**Anti-pattern:** Forcing construction for basic use; not providing convenience
functions; making the default mutable in ways that affect all users.
**Code example from source:**
```go
// net/http/client.go:108-109
// DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
var DefaultClient = &Client{}
// net/http/transport.go:47-58
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
```
---
## 9. Functional Configuration via Method Chaining (Scanner Pattern)
**Pattern name:** Post-Construction Configuration via Methods
**Source citation:** `bufio/scan.go` lines 275293
**What it does:** After construction with `NewScanner`, optional configuration is
applied via methods (`Split`, `Buffer`) before the first call to `Scan`.
**Why:** Keeps the constructor minimal (only the required `io.Reader`). Optional
configuration is discoverable via methods. Panics if called after scanning starts
(enforcing a construction → configure → use lifecycle).
**Anti-pattern:** Trying to pass all options into the constructor; allowing
configuration changes mid-use that corrupt state.
**Code example from source:**
```go
// bufio/scan.go:275-293
// Buffer sets the initial buffer to use when scanning
// and the maximum size of buffer that may be allocated during scanning.
// ...
// Buffer panics if it is called after scanning has started.
func (s *Scanner) Buffer(buf []byte, max int) {
if s.scanCalled {
panic("Buffer called after Scan")
}
s.buf = buf
s.maxTokenSize = max
}
// Split sets the split function for the [Scanner].
// ...
// Split panics if it is called after scanning has started.
func (s *Scanner) Split(split SplitFunc) {
if s.scanCalled {
panic("Split called after Scan")
}
s.split = split
}
```