diff --git a/patterns/configuration.md b/patterns/configuration.md new file mode 100644 index 0000000..cddbb1e --- /dev/null +++ b/patterns/configuration.md @@ -0,0 +1,989 @@ +# Go Configuration Patterns + +Patterns for configuring Go types and services, extracted from the +Go standard library source. + +**Source:** [golang/go](https://github.com/golang/go) at commit +[`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0) + +**Stats:** 33 Config/Options structs, 20 `With*` functions, 14 +`Default*` exports, 9 `Register*` functions in public stdlib. + +--- + +## 1. Zero-Value Usable Config Structs + +Struct with sensible defaults when all fields are zero. Users only +set what they need to change. + +### Source: + +[crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566) + +```go +// src/crypto/tls/common.go:566 +type Config struct { + // Rand provides the source of entropy for nonces and RSA blinding. + // If Rand is nil, TLS uses the cryptographic random reader in package + // crypto/rand. + Rand io.Reader + + // Time returns the current time as the number of seconds since the epoch. + // If Time is nil, TLS uses time.Now. + Time func() time.Time + + // Certificates contains one or more certificate chains to present to the + // other side of the connection. + Certificates []Certificate + // ... +} +``` + +Every field documents its zero-value behavior: "If nil, uses X." +The entire struct can be used as `&tls.Config{}` and it works. + +### Why + +Users only think about what they're changing. The stdlib handles +defaults internally. This eliminates "forgot to set field X" bugs +and makes constructor boilerplate unnecessary for simple cases. + +### When to Use + +**Triggers:** +- You're configuring a long-lived object (server, client, handler) +- Most users will only change 1-3 fields +- Each field has an obvious sensible default +- The struct will grow over time (backward compatibility via zero values) + +**Example — before:** +```go +// Without zero-value defaults — every user must know about every field +func NewServer(addr string, handler http.Handler, readTimeout time.Duration, + writeTimeout time.Duration, maxHeaderBytes int, tlsConfig *tls.Config, + errorLog *log.Logger) *Server { + return &Server{ + Addr: addr, Handler: handler, ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, MaxHeaderBytes: maxHeaderBytes, + TLSConfig: tlsConfig, ErrorLog: errorLog, + } +} + +// Caller must specify everything: +s := NewServer(":8080", mux, 30*time.Second, 30*time.Second, 1<<20, nil, nil) +``` + +**Example — after:** +```go +// With zero-value usable struct — users set only what they care about +s := &http.Server{ + Addr: ":8080", + Handler: mux, +} +// ReadTimeout, WriteTimeout, MaxHeaderBytes all have documented defaults. +// TLSConfig, ErrorLog use stdlib defaults when nil. +``` + +### When NOT to Use + +**Don't use this when:** +- There is no sensible default for a field (e.g., a database + connection string — there's no "default" database) +- The zero value is dangerous (e.g., `Timeout: 0` meaning "no + timeout" when you WANT a timeout by default) +- Users MUST make a conscious choice (use a constructor that + forces the required parameters) + +**Over-application example:** +```go +// Bad: zero value is DANGEROUS +type RetryConfig struct { + MaxRetries int // zero = no retries? or infinite retries? + Timeout time.Duration // zero = no timeout = hang forever +} + +// User creates: &RetryConfig{} — is that safe? Nobody knows. +``` + +**Better alternative:** +```go +// Required parameters in constructor, optional in struct +func NewRetrier(maxRetries int, timeout time.Duration) *Retrier { + if maxRetries <= 0 { + panic("maxRetries must be positive") + } + if timeout <= 0 { + panic("timeout must be positive") + } + return &Retrier{maxRetries: maxRetries, timeout: timeout} +} +``` + +### Anti-pattern + +```go +// DON'T: Config struct with fields that mean different things at zero +type Config struct { + Port int // 0 = random port? or invalid? or default 8080? +} + +// DO: Document and handle zero explicitly +type Config struct { + // Port specifies the TCP port to listen on. + // If zero, defaults to 8080. + Port int +} +``` + +--- + +## 2. Options Struct as Function Parameter + +Pass an exported struct of optional settings to a constructor or +method. + +### Source: + +[log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135) + +```go +// src/log/slog/handler.go:135 +type HandlerOptions struct { + // AddSource causes the handler to compute the source code position + // of the log statement and add a SourceKey attribute to the output. + AddSource bool + + // Level reports the minimum record level that will be logged. + // The handler discards records with lower levels. + // If Level is nil, the handler assumes LevelInfo. + Level Leveler + + // ReplaceAttr is called to rewrite each non-group attribute + // before it is logged. + ReplaceAttr func(groups []string, a Attr) Attr +} +``` + +Usage: +```go +h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + Level: slog.LevelDebug, +}) +``` + +### Why + +Separates required arguments (the writer) from optional configuration. +The struct can be `nil` (use all defaults) or partially filled. +Adding fields later doesn't break callers. + +### When to Use + +**Triggers:** +- You have 3+ optional parameters for a constructor +- Parameters are related and configure the same subsystem +- Users will often use defaults for most of them +- You need to be able to add options without breaking callers + +**Example — before:** +```go +// Growing parameter list — every new option breaks all callers +func NewHandler(w io.Writer, addSource bool, level Level, + replaceAttr func([]string, Attr) Attr) *Handler { ... } + +// Every caller must pass all args even for defaults: +h := NewHandler(os.Stdout, false, LevelInfo, nil) +``` + +**Example — after:** +```go +// Options struct — nil means "all defaults" +func NewHandler(w io.Writer, opts *HandlerOptions) *Handler { ... } + +// Simple case — no options needed: +h := slog.NewJSONHandler(os.Stdout, nil) + +// Custom case — only set what you need: +h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, +}) +``` + +### When NOT to Use + +**Don't use this when:** +- You have 1-2 optional parameters (just use direct params with + zero-value semantics) +- The options change per-call, not per-instance (use functional + options for per-call variation) +- Every user needs different options (nothing is truly "optional") + +**Over-application example:** +```go +// Unnecessary: only one option, and it's always set +type ParseOptions struct { + Format string +} + +func Parse(input string, opts *ParseOptions) (*Result, error) { + format := "json" // default + if opts != nil { + format = opts.Format + } + // ... +} + +// Every caller writes: Parse(input, &ParseOptions{Format: "yaml"}) +// This is MORE awkward than: Parse(input, "yaml") +``` + +**Better alternative:** +```go +// When there's really only one option, make it a parameter: +func Parse(input string, format string) (*Result, error) { ... } +``` + +--- + +## 3. Functional Options (With* Pattern) + +Functions that return an opaque Options type, composed via variadic +parameters. + +### Source: + +[encoding/json/jsontext/options.go#L232](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/jsontext/options.go#L232) + +```go +// src/encoding/json/jsontext/options.go:232 +func WithIndent(indent string) Options { + // ... +} + +// src/encoding/json/jsontext/encode.go:91 +func NewEncoder(w io.Writer, opts ...Options) *Encoder { + e := new(Encoder) + e.Reset(w, opts...) + return e +} +``` + +Usage: +```go +enc := jsontext.NewEncoder(w, + jsontext.WithIndent(" "), + jsontext.WithByteLimit(1024*1024), +) +``` + +### Why + +Options can be added over time without breaking callers. Each option +is self-documenting (the function name says what it does). Options +can be pre-composed and reused. The zero-option case reads cleanly: +`NewEncoder(w)`. + +### When to Use + +**Triggers:** +- The option set will grow over time (new features, new modes) +- Options should be individually composable and reusable +- Per-call configuration (not just per-instance) +- You want option names in the API surface (not struct field names) + +**Example — before:** +```go +// Options struct works but gets unwieldy with many fields: +enc := json.NewEncoder(w, &json.EncoderOptions{ + Indent: " ", + ByteLimit: 1024*1024, + DepthLimit: 100, + EscapeHTML: true, + SortMapKeys: true, +}) +``` + +**Example — after:** +```go +// Functional options — compose only what you need: +enc := json.NewEncoder(w, + json.WithIndent(" "), + json.WithByteLimit(1024*1024), +) + +// Pre-compose for reuse: +var prettyJSON = []json.Options{ + json.WithIndent(" "), + json.WithByteLimit(10*1024*1024), +} +enc := json.NewEncoder(w, prettyJSON...) +``` + +### When NOT to Use + +**Don't use this when:** +- You have <3 options that won't grow (use direct parameters or + an options struct) +- Options interact with each other in complex ways (a struct makes + dependencies visible; functional options hide them) +- Users need to inspect/read back the configuration (options are + typically write-only — you can set them but not query them) + +**Over-application example:** +```go +// Overkill for 2 stable options: +func Connect(addr string, opts ...ConnectOption) (*Conn, error) + +// Every caller writes: +conn, _ := Connect("localhost:5432", + WithTimeout(5*time.Second), + WithTLS(true), +) +// vs simply: +conn, _ := Connect("localhost:5432", 5*time.Second, true) +// or: +conn, _ := Connect("localhost:5432", &ConnectOptions{ + Timeout: 5*time.Second, TLS: true, +}) +``` + +**Better alternative:** Use an options struct when: +- The option set is stable (<5 options) +- Users need to read options back +- Options interact (struct makes co-dependencies visible) + +--- + +## 4. Package-Level Default Instances + +A package provides a pre-configured, ready-to-use instance. + +### Source: + +[net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L109) + +```go +// src/net/http/client.go:109 +var DefaultClient = &Client{} + +// src/net/http/transport.go:47 +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, +} + +// src/log/slog/logger.go:55 +func Default() *Logger { return defaultLogger.Load() } +``` + +### Why + +Most programs need exactly one instance with default settings. The +package-level default eliminates boilerplate for the common case +while still allowing custom instances for tests or specialized needs. + +### When to Use + +**Triggers:** +- 90% of users will use the default configuration +- The type is safe for concurrent use +- Creating an instance requires non-trivial setup (transport pools, + connection config) +- Package-level functions exist that delegate to the default + +**Example — before:** +```go +// Without default — every caller must create and configure +client := &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 100, + // ... 10 more fields for reasonable defaults + }, +} +resp, err := client.Get(url) +``` + +**Example — after:** +```go +// With package-level default — simple case is one line +resp, err := http.Get(url) // uses http.DefaultClient internally +``` + +### When NOT to Use + +**Don't use this when:** +- Every user needs different configuration (no meaningful default) +- The instance holds resources that should be explicitly closed +- Global mutable state would cause test interference +- The type is NOT safe for concurrent use + +**Over-application example:** +```go +// Bad: default database connection — there IS no universal default +var DefaultDB = MustConnect("postgres://localhost/mydb") +// What database? What credentials? This makes no sense as a default. +``` + +**Better alternative:** +```go +// Force users to be explicit about connections +db, err := sql.Open("postgres", connString) +``` + +### Anti-pattern + +```go +// DON'T: Mutable default that tests can't isolate +var DefaultLogger = NewLogger(os.Stdout) +// Tests that modify DefaultLogger race with each other + +// DO: Immutable default with replacement via function +func Default() *Logger { return defaultLogger.Load() } +func SetDefault(l *Logger) { defaultLogger.Store(l) } +// Atomic replacement — tests can use SetDefault safely +``` + +--- + +## 5. Init-Time Registration + +Plugins/drivers register themselves in `init()`, looked up at runtime +by name. + +### Source: + +[database/sql/sql.go#L53](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L53) + +```go +// src/database/sql/sql.go:53 +func Register(name string, driver driver.Driver) { + driversMu.Lock() + defer driversMu.Unlock() + if driver == nil { + panic("sql: Register driver is nil") + } + if _, dup := drivers[name]; dup { + panic("sql: Register called twice for driver " + name) + } + drivers[name] = driver +} +``` + +Driver packages register in init: +```go +// In the driver package (e.g., github.com/lib/pq): +func init() { + sql.Register("postgres", &Driver{}) +} +``` + +Users import for side effects: +```go +import ( + "database/sql" + _ "github.com/lib/pq" // registers "postgres" driver +) + +db, err := sql.Open("postgres", connString) +``` + +### Why + +Decouples the framework from implementations. The `database/sql` +package doesn't import any driver — drivers import IT and register. +New drivers can be added without changing the framework. + +### When to Use + +**Triggers:** +- You're building a framework/registry with pluggable backends +- Implementations are in separate packages (compile-time decoupling) +- Users choose implementations at link time (import selection) +- The set of implementations is open-ended + +**Example — before:** +```go +// Hard-coded implementations — every new driver requires editing this +func Open(driverName, dataSourceName string) (*DB, error) { + switch driverName { + case "postgres": + return openPostgres(dataSourceName) + case "mysql": + return openMySQL(dataSourceName) + // Can't add new drivers without modifying this code + } +} +``` + +**Example — after:** +```go +// Registration pattern — open to extension, closed to modification +func Open(driverName, dataSourceName string) (*DB, error) { + driver, ok := drivers[driverName] + if !ok { + return nil, fmt.Errorf("sql: unknown driver %q", driverName) + } + return driver.Open(dataSourceName) +} +// New drivers register themselves — zero changes to this code. +``` + +### When NOT to Use + +**Don't use this when:** +- You control all implementations (use interfaces directly) +- Registration order matters (init order is non-deterministic across + packages) +- You need to test without global state pollution +- The "plugin" needs configuration beyond just existing + +**Over-application example:** +```go +// Over-engineered for an internal app with 2 known implementations +var handlers = map[string]Handler{} +func Register(name string, h Handler) { handlers[name] = h } + +func init() { Register("json", &JSONHandler{}) } +func init() { Register("xml", &XMLHandler{}) } + +// These are always the same two. Just use a constructor: +func NewHandler(format string) Handler { + switch format { + case "json": return &JSONHandler{} + case "xml": return &XMLHandler{} + } +} +``` + +### Anti-pattern + +```go +// DON'T: Registration without duplicate detection +func Register(name string, d Driver) { + drivers[name] = d // silently overwrites — last-import-wins +} + +// DO: Panic on duplicate (from database/sql) +if _, dup := drivers[name]; dup { + panic("sql: Register called twice for driver " + name) +} +``` + +--- + +## 6. Context-Carried Configuration (WithValue) + +Request-scoped configuration passed through context.Context. + +### Source: + +[context/context.go#L728](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L728) + +```go +// src/context/context.go:728 +func WithValue(parent Context, key, val any) Context { + if parent == nil { + panic("cannot create context from nil parent") + } + if key == nil { + panic("nil key") + } + if !reflectlite.TypeOf(key).Comparable() { + panic("key is not comparable") + } + return &valueCtx{parent, key, val} +} +``` + +Usage with unexported key type (the idiom): +```go +// src/net/http/httptrace/trace.go:33 +type clientEventContextKey struct{} + +func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context { + // ... + return context.WithValue(ctx, clientEventContextKey{}, trace) +} +``` + +### Why + +Passes request-scoped data through call chains without adding +parameters to every function signature. The unexported key type +prevents collision between packages. + +### When to Use + +**Triggers:** +- Data is request-scoped (trace ID, auth token, deadline) +- Data must cross package boundaries without coupling them +- The data is "ambient" (needed by middleware/infrastructure, not + business logic) +- Adding a parameter to every function in the chain is impractical + +**Example — before:** +```go +// Propagating trace ID through 5 layers of function calls: +func HandleRequest(traceID string, r *Request) { + result := processOrder(traceID, r.Order) + notify(traceID, result) +} +func processOrder(traceID string, o Order) Result { + validated := validate(traceID, o) + return persist(traceID, validated) +} +// traceID is threaded through EVERY function — pollutes all signatures +``` + +**Example — after:** +```go +type traceIDKey struct{} + +func WithTraceID(ctx context.Context, id string) context.Context { + return context.WithValue(ctx, traceIDKey{}, id) +} + +func HandleRequest(ctx context.Context, r *Request) { + result := processOrder(ctx, r.Order) + notify(ctx, result) +} +// traceID travels invisibly in ctx — only extracted where needed +``` + +### When NOT to Use + +**Don't use this when:** +- The data is required for correctness (make it an explicit param — + context values are invisible, easy to forget) +- The data is needed in EVERY function (it's not ambient, it's core) +- You're using it to avoid adding a parameter to 2-3 functions + (that's not enough pain to justify the indirection) +- The data is mutable (context values are immutable by convention) + +**Over-application example:** +```go +// Bad: config that EVERY function needs — should be a field +type serverConfig struct{} + +func handleRequest(ctx context.Context, r *Request) { + cfg := ctx.Value(serverConfig{}).(*Config) + // Every handler digs into context for core config + // This is just dependency injection with extra steps and no type safety +} +``` + +**Better alternative:** +```go +// Make it a field on the server/handler: +type Server struct { + config *Config +} +func (s *Server) handleRequest(r *Request) { + // s.config is always there, typed, visible +} +``` + +### Anti-pattern + +```go +// DON'T: Exported key type (allows collision) +var TraceKey = "trace-id" // any package can use this string + +// DO: Unexported struct type (package-scoped, collision-proof) +type traceKey struct{} +``` + +--- + +## 7. Builder Pattern via Method Chaining + +Not a stdlib pattern — but its ABSENCE is instructive. + +### Source: + +The Go stdlib does NOT use builder patterns. Zero instances of +method chaining for configuration exist in the public API. + +### Why + +Go prefers struct literals for construction. Builders hide what's +being set, make types non-trivially copyable, and create an +awkward "build phase" vs "use phase" distinction. + +### When to Use + +**Almost never in Go.** The only legitimate case: +- Building immutable objects where the construction process is + genuinely complex (>10 steps with conditionals) +- Even then, prefer a config struct + constructor. + +### When NOT to Use + +**Don't use this when:** +- A struct literal works (always try struct literal first) +- You're porting patterns from Java/C# (they use builders because + they lack struct literals with named fields) +- You want "fluent" APIs (Go culture values explicit over clever) + +**Over-application example:** +```go +// Java-brain in Go: +server := NewServerBuilder(). + WithAddr(":8080"). + WithTimeout(30 * time.Second). + WithHandler(mux). + WithTLS(cert, key). + Build() + +// In Go, this is just: +server := &http.Server{ + Addr: ":8080", + ReadTimeout: 30 * time.Second, + Handler: mux, + TLSConfig: tlsConfig, +} +// Clearer, no hidden state, no build/use phase split. +``` + +--- + +## 8. Exported Fields with Documented Nil Behavior + +Config fields that accept function values, with nil meaning "use +default behavior." + +### Source: + +[crypto/tls/common.go#L572](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L572) + +```go +// src/crypto/tls/common.go:572 +// Time returns the current time as the number of seconds since the epoch. +// If Time is nil, TLS uses time.Now. +Time func() time.Time +``` + +Also: [log/slog/handler.go#L169](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L169) +```go +// ReplaceAttr is called to rewrite each non-group attribute before +// it is logged. If ReplaceAttr returns a zero Attr, the attribute +// is discarded. +ReplaceAttr func(groups []string, a Attr) Attr +``` + +### Why + +Allows injection of custom behavior without requiring interfaces. +The nil check is simpler than defining an interface, implementing a +default, and wiring it. Good for hooks where most users want the +default but advanced users need to customize. + +### When to Use + +**Triggers:** +- Optional behavior customization (not every user needs it) +- The "interface" would have exactly one method +- Default behavior is obvious (time.Now, os.Stderr, etc.) +- Function signature is stable + +**Example — before:** +```go +// Interface for one method — ceremony for no gain +type TimeProvider interface { + Now() time.Time +} +type defaultTimeProvider struct{} +func (d defaultTimeProvider) Now() time.Time { return time.Now() } + +type Config struct { + TimeProvider TimeProvider // required interface, must set +} +``` + +**Example — after:** +```go +// Function field — nil means default +type Config struct { + // Time returns the current time. + // If nil, time.Now is used. + Time func() time.Time +} + +// Usage in implementation: +func (c *Config) now() time.Time { + if c.Time != nil { + return c.Time() + } + return time.Now() +} +``` + +### When NOT to Use + +**Don't use this when:** +- The function has side effects that need lifecycle management + (use an interface with Close) +- Multiple methods are needed together (use an interface) +- The function needs to carry state (use a struct implementing + an interface) + +--- + +## 9. Immutable-After-Use Convention + +Config structs that must not be modified after being passed to a +constructor. + +### Source: + +[crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566) + +```go +// A Config structure is used to configure a TLS client or server. +// After one has been passed to a TLS function it must not be modified. +// A Config may be reused; the tls package will also not modify it. +type Config struct { ... } +``` + +### Why + +Avoids defensive copying of large structs. The TLS config has 30+ +fields — copying on every handshake would waste memory. Instead, +the contract is: "you give it to us, you stop touching it." + +### When to Use + +**Triggers:** +- Config struct is large (>5 fields) +- Copying would be expensive (contains slices, maps, or pointers) +- The configured object is long-lived (server, pool, transport) +- Concurrent access to config is possible + +**Example — before:** +```go +// Defensive copy — safe but expensive for large configs +func NewServer(cfg Config) *Server { + cfgCopy := cfg // copies all fields + return &Server{config: &cfgCopy} +} +``` + +**Example — after:** +```go +// Document immutability constraint — no copy needed +// "After one has been passed to NewServer it must not be modified." +func NewServer(cfg *Config) *Server { + return &Server{config: cfg} // shared reference, caller must not mutate +} +``` + +### When NOT to Use + +**Don't use this when:** +- The struct is small and cheap to copy (just copy it) +- Users frequently need to create variations (provide a `Clone()` + method instead) +- The contract is hard to enforce (tests can't catch violations) + +--- + +## 10. Clone for Config Variation + +Provide a `Clone()` method when users need to create modified copies +of immutable-after-use configs. + +### Source: + +[crypto/tls/common.go#L925](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L925) (tls.Config.Clone) + +```go +// Clone returns a shallow clone of c or nil if c is nil. It is safe to clone +// a Config that is being used concurrently by a TLS client or server. +func (c *Config) Clone() *Config { + if c == nil { + return nil + } + c.mutex.Lock() + defer c.mutex.Unlock() + return &Config{ + Rand: c.Rand, + Time: c.Time, + Certificates: c.Certificates, + // ... all fields copied + } +} +``` + +### Why + +When a config is immutable-after-use but users need variations +(e.g., same TLS config but different ServerName for each host), +Clone gives them a safe way to fork without modifying the original. + +### When to Use + +**Triggers:** +- You have immutable-after-use config structs +- Users need slight variations of a base config +- The struct contains reference types (slices, maps) that need + safe copying +- The struct has unexported fields or a mutex + +**Example — before:** +```go +// Without Clone — users attempt (broken) manual copy +baseCfg := &tls.Config{MinVersion: tls.VersionTLS12} +// Can't just: hostCfg := *baseCfg (unexported fields, shared slices) +``` + +**Example — after:** +```go +baseCfg := &tls.Config{MinVersion: tls.VersionTLS12} +hostCfg := baseCfg.Clone() +hostCfg.ServerName = "example.com" +``` + +### When NOT to Use + +**Don't use this when:** +- The struct has no unexported fields and no reference types + (plain struct copy `*s` works fine) +- Users rarely need variations (one config for the whole app) + +--- + +## Summary: Configuration Decision Tree + +``` +Is the configuration per-instance or per-call? +├── Per-instance → struct-based patterns (#1, #2, #8, #9, #10) +│ ├── <3 options? → Direct parameters +│ ├── 3-10 options, stable? → Options struct (#2) +│ ├── Long-lived, most fields have defaults? → Zero-value config (#1) +│ ├── Must not mutate after use? → Immutable convention (#9) + Clone (#10) +│ └── Hook/callback injection? → Function fields (#8) +├── Per-call → functional patterns (#3, #6) +│ ├── Options will grow? → Functional options / With* (#3) +│ └── Request-scoped ambient data? → Context values (#6) +└── Framework/plugin boundary? → Registration (#5) + └── Global default for common case? → Default instance (#4) +``` + +**Key principle:** Start with the simplest pattern that works. Only +reach for functional options or registration when you have evidence +the option set is growing or implementations are external. + +See also: +- [interfaces.md](interfaces.md) — Accept interfaces, return structs +- [api-conventions.md](api-conventions.md) — Backward compatibility + + diff --git a/patterns/from-source.md b/patterns/from-source.md deleted file mode 100644 index 37f4a67..0000000 --- a/patterns/from-source.md +++ /dev/null @@ -1,794 +0,0 @@ -# Go Patterns (from golang/go Source) - -Prescriptive patterns extracted from the Go language source using -iterative analysis. Real examples, hyperlinked to source. - -**Source:** [golang/go](https://github.com/golang/go) at commit -[`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0) - -**Stats:** 281 interfaces, 55 sentinel errors, 145 error types, -262 constructors, 309 context-accepting functions, 1,065 examples. - ---- - -## Interface Design - -### Single-Method Interfaces - -**Rule:** Define interfaces with exactly one method whenever possible. - -```go -type Reader interface { - Read(p []byte) (n int, err error) -} -``` - -**Why:** Any type with that method satisfies the interface implicitly. -Smaller interfaces = more types satisfy them = more reusable code. - -**When to use:** Defining abstraction boundaries, function parameters, -dependency injection. - -**When NOT to use:** When operations are genuinely inseparable -(`sort.Interface` needs Len+Less+Swap together). - -**Source:** [src/io/io.go#L86](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L86) - ---- - -### Interface Composition - -**Rule:** Build larger interfaces by embedding smaller ones. - -```go -type ReadWriter interface { - Reader - Writer -} - -type ReadWriteCloser interface { - Reader - Writer - Closer -} -``` - -**Why:** 15 composed interfaces in `io/io.go` from just 4 primitives -(Reader, Writer, Closer, Seeker). Composition prevents interface -bloat. - -**When to use:** When callers need multiple capabilities together. - -**When NOT to use:** Don't compose preemptively. Add compositions -only when you have a real function that needs both capabilities. - -**Source:** [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131) - ---- - -### Accept Interfaces, Return Structs - -**Rule:** Parameters should be interfaces. Return values should be -concrete types. - -```go -// Accepts interface: -func Copy(dst Writer, src Reader) (written int64, err error) - -// Returns concrete: -func NewReader(rd io.Reader) *Reader -``` - -**Why:** Accepting interfaces maximizes caller flexibility. Returning -structs gives callers full access without type assertions. 262 `New*` -constructors in stdlib all return concrete types. - -**When to use:** Every public API function. - -**When NOT to use:** When the return type must be hidden (use an -interface to prevent users from depending on internals). - -**Source:** [src/io/io.go#L408](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L408) (Copy), [src/bufio/bufio.go](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/bufio.go#L62) (NewReader) - ---- - -### The Stringer Interface - -**Rule:** Implement `String() string` for any type that has a human- -readable representation. - -```go -func (t Time) String() string { - return t.Format("2006-01-02 15:04:05.999999999 -0700 MST") -} -``` - -**Why:** 379 types in stdlib implement Stringer. `fmt.Println` uses it -automatically. It's the Go equivalent of `__str__`. - -**When to use:** Any type that might be printed or logged. - -**When NOT to use:** Internal types that are never user-visible. - -**Source:** [src/time/time.go](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go) (Time.String) - ---- - -### Type Assertion for Optional Interfaces - -**Rule:** Check if a value implements an optional interface using -type assertion. - -```go -if wt, ok := src.(WriterTo); ok { - return wt.WriteTo(dst) -} -``` - -**Why:** 104 type assertions in stdlib. This pattern allows fallback -behavior — try the fast path, fall back to the generic path. - -**When to use:** Optional optimizations (WriterTo, ReaderFrom), feature -detection. - -**When NOT to use:** Required behavior (just accept the interface -directly in the signature). - -**Source:** [src/io/io.go#L420](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L420) (Copy's WriterTo check) - ---- - -## Error Handling - -### Sentinel Errors for Known Conditions - -**Rule:** Package-level `var Err*` for errors callers need to check. -Include package name in the message. - -```go -var ErrBadPattern = errors.New("syntax error in pattern") -var ErrRange = errors.New("value out of range") -var ErrUnsupported = errors.New("unsupported operation") -``` - -**Why:** 55 exported sentinel errors in stdlib. Callers use -`errors.Is(err, strconv.ErrRange)` to handle specific cases. - -**When to use:** Errors that represent documented, expected conditions -callers should distinguish. - -**When NOT to use:** Errors that carry dynamic context (use error -types). Errors callers never need to identify specifically. - -**Source:** [src/strconv/number.go#L246](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strconv/number.go#L246) - ---- - -### Error Types for Rich Context - -**Rule:** Define types implementing `error` when you need structured -error information. - -```go -type PathError struct { - Op string - Path string - Err error -} - -func (e *PathError) Error() string { - return e.Op + " " + e.Path + ": " + e.Err.Error() -} - -func (e *PathError) Unwrap() error { return e.Err } -``` - -**Why:** 145 error type implementations in stdlib. Callers use -`errors.As(err, &pathErr)` to extract structured data. - -**When to use:** When the error needs to carry structured fields -(path, operation, underlying error). - -**When NOT to use:** Simple conditions (use sentinel errors). One-off -errors (use `fmt.Errorf`). - -**Source:** [src/os/error.go](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/error.go) (PathError) - ---- - -### Wrap with %w - -**Rule:** Add context when propagating errors. Use `%w` to preserve -the chain. - -```go -return fmt.Errorf("cannot parse %q as JSON number: %w", val, strconv.ErrSyntax) -``` - -**Why:** 115 `%w` wrappings in stdlib. Creates a chain that -`errors.Is` and `errors.As` can traverse. - -**When to use:** Every time you add context to an error from a lower -layer. - -**When NOT to use:** When the original error's identity should be -hidden from callers (use `%v` to break the chain). - -**Source:** [src/encoding/json/v2_decode.go#L219](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/v2_decode.go#L219) - ---- - -### io.EOF as Termination Signal - -**Rule:** Use `io.EOF` to signal normal end-of-stream, not an error. - -```go -n, err := r.Read(buf) -if err == io.EOF { - break // Normal termination -} -if err != nil { - return err // Actual error -} -``` - -**Why:** 316 `io.EOF` references in stdlib. EOF is expected, not -exceptional. Readers return io.EOF when there's no more data. - -**When to use:** Implementing Reader, iterators, stream processors. - -**When NOT to use:** Errors that indicate failure (use a real error). - -**Source:** [src/io/io.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L44) - ---- - -## Testing - -### Table-Driven Tests - -**Rule:** Use `[]struct{}` with named cases and `t.Run`. - -```go -tests := []struct { - name string - input string - want string -}{ - {"empty", "", ""}, - {"hello", "hello", "HELLO"}, -} -for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := Transform(tt.input) - if got != tt.want { - t.Errorf("got %q, want %q", got, tt.want) - } - }) -} -``` - -**Why:** 1,926 `t.Run` calls in the Go source. Named subtests make -failure output clear. Adding cases is one struct literal. - -**When to use:** Any function with 3+ input variations. - -**When NOT to use:** Tests where setup varies significantly between -cases (separate test functions). - -**Source:** [src/testing/testing_test.go](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing_test.go) (TestSetenv) - ---- - -### t.Helper() for Test Utilities - -**Rule:** Call `t.Helper()` as the first line of any test helper. - -```go -func assertEqual(t *testing.T, got, want string) { - t.Helper() - if got != want { - t.Errorf("got %q, want %q", got, want) - } -} -``` - -**Why:** 2,685 `t.Helper()` calls. Without it, error messages report -the helper's line number instead of the caller's. - -**When to use:** Every function that calls `t.Error`, `t.Fatal`, or -other testing.T methods on behalf of the caller. - -**When NOT to use:** Functions that ARE the test (not helpers). - ---- - -### Example Functions as Living Docs - -**Rule:** Write `Example*` functions in `_test.go` with `// Output:` -comments. - -```go -func ExampleSprintf() { - fmt.Println(fmt.Sprintf("Hello, %s", "world")) - // Output: Hello, world -} -``` - -**Why:** 1,065 Example functions in stdlib. They compile, run, and -appear in docs. They can't go stale. - -**When to use:** Every exported function that would benefit from usage -demonstration. - -**When NOT to use:** Internal APIs. Functions with non-deterministic -output. - ---- - -### testdata/ for Fixtures - -**Rule:** Put test fixtures in `testdata/` directories. - -**Why:** 111 `testdata/` dirs in stdlib. The go tool ignores them -during compilation. Golden files, certificates, sample inputs live -here. - -**When to use:** Files your tests read but never modify at runtime. - -**When NOT to use:** Generated test data (create in TestMain). - ---- - -### Benchmarks - -**Rule:** Prefix benchmark functions with `Benchmark` and use `b.N`. - -```go -func BenchmarkSprintf(b *testing.B) { - for b.Loop() { - fmt.Sprintf("hello, %s", "world") - } -} -``` - -**Why:** 1,974 benchmark functions in stdlib. Performance is tested, -not assumed. - -**When to use:** Any code on a hot path. Any code you're optimizing. - -**When NOT to use:** Code that's not performance-sensitive. - ---- - -## Package Organization - -### Flat Packages - -**Rule:** No `pkg/` wrapper. Import path = directory path. - -``` -myproject/ -├── server/ -├── client/ -├── internal/ -└── cmd/ - └── myapp/ -``` - -**Why:** The Go stdlib has zero nesting beyond 2 levels (e.g., -`net/http`). Import paths should be short and predictable. - -**When to use:** Always. - -**When NOT to use:** Never. `pkg/` is a community anti-pattern the Go -team never endorsed. - ---- - -### internal/ for Shared Private Code - -**Rule:** Code shared between packages but not part of public API -goes in `internal/`. - -**Why:** 61 internal packages in stdlib. Compiler-enforced — external -code cannot import them. Stronger than unexported identifiers. - -**When to use:** Utility code that multiple packages need but users -shouldn't depend on. - -**When NOT to use:** Code only one package uses (keep it unexported -in that package). Code stable enough for public API (promote it). - ---- - -### One Package, One Responsibility - -**Rule:** A package does one thing. Name it with a singular noun. - -`fmt`, `io`, `net`, `os`, `sync`, `time`, `bytes`, `errors` - -**Why:** Package names prefix all exported identifiers. Short names -compose well: `bytes.Buffer`, `sync.Mutex`, `time.Duration`. - -**When to use:** Every package. - -**When NOT to use:** Never name packages `utils`, `helpers`, `common`, -`models`, or `types`. - ---- - -## Concurrency - -### context.Context as First Parameter - -**Rule:** Functions that do I/O take `ctx context.Context` first. - -```go -func (c *Client) Do(ctx context.Context, req *Request) (*Response, error) -``` - -**Why:** 309 functions take Context in stdlib. First-parameter position -is universal. Context carries cancellation, deadlines, values. - -**When to use:** Any function that blocks, does I/O, or might be -cancelled. - -**When NOT to use:** Pure computation. Init functions. Functions that -complete instantly. - ---- - -### sync.Mutex for Shared State - -**Rule:** Protect shared state with a mutex. Comment what it guards. - -```go -type Group struct { - mu sync.Mutex // protects m - m map[string]*call // lazily initialized -} -``` - -**Why:** 148 Mutex/RWMutex fields in stdlib. The comment-what-it- -guards pattern appears throughout. - -**When to use:** Shared mutable state accessed by multiple goroutines. - -**When NOT to use:** Channel-based coordination. Single-goroutine -ownership. - -**Source:** [src/internal/singleflight/singleflight.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/internal/singleflight/singleflight.go#L30) - ---- - -### sync.Once for Lazy Initialization - -**Rule:** Use `sync.Once` for thread-safe lazy init. - -```go -var defaultLogger struct { - once sync.Once - val *Logger -} - -func getLogger() *Logger { - defaultLogger.once.Do(func() { - defaultLogger.val = newLogger() - }) - return defaultLogger.val -} -``` - -**Why:** 58 `sync.Once` usages in stdlib. Guarantees exactly-once -execution regardless of concurrent callers. - -**When to use:** Expensive initialization that should happen at most -once (DB connections, compiled regexps, parsed configs). - -**When NOT to use:** Init that should happen at package load (use -`init()` or package-level `var`). - ---- - -### sync.Pool for Reusable Buffers - -**Rule:** Use `sync.Pool` for frequently allocated/freed objects. - -```go -var encodeStatePool sync.Pool - -func newEncodeState() *encodeState { - if v := encodeStatePool.Get(); v != nil { - e := v.(*encodeState) - e.Reset() - return e - } - return new(encodeState) -} -``` - -**Why:** Used in encoding/json, fmt, and other hot-path code. Reduces -GC pressure by reusing allocations. - -**When to use:** Objects allocated per-request that are expensive to -create and safe to reuse. - -**When NOT to use:** Small objects. Objects with complex cleanup. -Objects that shouldn't be shared between goroutines. - -**Source:** [src/encoding/json/encode.go#L312](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L312) - ---- - -### defer for Cleanup - -**Rule:** Use `defer` immediately after acquiring a resource. - -```go -mu.Lock() -defer mu.Unlock() - -f, err := os.Open(path) -if err != nil { return err } -defer f.Close() -``` - -**Why:** 329 `defer Close()`/`defer Unlock()` in stdlib. Guarantees -cleanup even on panic. Pairs acquisition with release visually. - -**When to use:** Every Lock/Close/Release/Done pattern. - -**When NOT to use:** Hot loops where defer overhead matters (rare, -profile first). - ---- - -## Documentation - -### Package-Level doc.go - -**Rule:** Complex packages get a `doc.go` with overview documentation. - -```go -// Source: src/fmt/doc.go -/* -Package fmt implements formatted I/O with functions analogous -to C's printf and scanf. -*/ -package fmt -``` - -**Why:** 25 `doc.go` files in stdlib. Separates overview from code. -`#` headings create sections in pkg.go.dev. - -**When to use:** Any package with non-trivial API surface. - -**When NOT to use:** Small packages where the comment fits in the main -file. - ---- - -### Deprecated: Comment Convention - -**Rule:** Mark deprecated items with `// Deprecated: use X instead.` - -**Why:** 203 `Deprecated:` comments in stdlib. Tools (editors, linters) -recognize this pattern and show warnings. - -**When to use:** Any public API you want to discourage but can't -remove. - -**When NOT to use:** Internal code (just delete it). - ---- - -## Naming - -### Short Package Names - -**Rule:** 3-7 characters, lowercase, singular noun. - -`fmt` · `io` · `net` · `os` · `sync` · `time` · `bytes` · `errors` - -**When to use:** Every package. - -**When NOT to use:** NEVER: `utils`, `helpers`, `common`, `base`, -`models`, `types`, `shared`. - ---- - -### New* Constructors - -**Rule:** Constructor functions are named `New` or `New`. - -```go -func NewReader(rd io.Reader) *Reader -func New(text string) error -``` - -**Why:** 262 `New*` functions in stdlib. Universal convention. No -`Create*`, no `Make*` (except `make` builtin), no `Build*`. - -**When to use:** Any function that allocates and initializes. - -**When NOT to use:** Functions that transform or convert (name them -by what they do: `Parse`, `Open`, `Dial`). - ---- - -### No Get Prefix - -**Rule:** Getters don't say "Get". Setters DO say "Set". - -```go -// Wrong: -func (u *User) GetName() string - -// Right: -func (u *User) Name() string -func (u *User) SetName(s string) -``` - -**Why:** Go convention. Only 58 `Get*` methods in all of stdlib -(mostly in legacy APIs like `net/http`). - -**When to use:** All accessor methods. - -**When NOT to use:** RPC/protobuf generated code (follows its own -convention). - ---- - -### MixedCaps Only - -**Rule:** `ExportedName` and `unexportedName`. Never underscores. - -**Why:** Capitalization IS the visibility system. Underscores are -reserved for test files and generated code. - ---- - -## Configuration - -### Functional Options (With* Pattern) - -**Rule:** Options as functions returning an opaque Options type. - -```go -func NewEncoder(w io.Writer, opts ...Options) *Encoder - -// Option constructors: -func WithIndent(indent string) Options { ... } -func WithByteLimit(n int64) Options { ... } -``` - -**Why:** Growing in stdlib (encoding/json/v2, context). Allows adding -options without breaking existing callers. - -**When to use:** APIs with many optional parameters that grow over -time. - -**When NOT to use:** Simple APIs with 1-2 options (just use parameters -or a config struct). - -**Source:** [src/encoding/json/jsontext/options.go#L232](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/jsontext/options.go#L232) - ---- - -### Config Structs for Complex Setup - -**Rule:** Group related options into a named struct. - -```go -type Config struct { - Certificates []Certificate - RootCAs *x509.CertPool - ServerName string - MinVersion uint16 -} -``` - -**Why:** `crypto/tls.Config` is the canonical example. Zero value is -usable with sensible defaults. - -**When to use:** APIs with many related settings that configure a -long-lived object. - -**When NOT to use:** Per-call options (use functional options). - -**Source:** [src/crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566) - ---- - -## Extension - -### Register Pattern (Plugin Discovery) - -**Rule:** Provide a `Register*` function for plugin architectures. - -```go -func Register(name string, driver driver.Driver) { - // ... -} -``` - -**Why:** Used in `database/sql`, `encoding/gob`, `image`, -`archive/zip`, `crypto`. The pattern: init-time registration + -runtime lookup. - -**When to use:** When users provide implementations you discover at -runtime (drivers, codecs, formats). - -**When NOT to use:** When you know all implementations at compile -time (use interfaces directly). - -**Source:** [src/database/sql/sql.go#L53](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L53) - ---- - -## Performance - -### Append* for Zero-Alloc Formatting - -**Rule:** Provide `Append*` variants that write to caller's buffer. - -```go -func (t Time) AppendFormat(b []byte, layout string) []byte -func AppendEncode(dst, src []byte) []byte -func AppendQuote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, error) -``` - -**Why:** Growing pattern in stdlib. Avoids allocation by letting the -caller own the buffer. The `encoding` package now defines -`BinaryAppender` and `TextAppender` interfaces. - -**When to use:** Hot-path formatting functions where allocation cost -matters. - -**When NOT to use:** Convenience APIs where readability > performance. - -**Source:** [src/time/format.go#L655](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/format.go#L655) - ---- - -### Preallocate Slices - -**Rule:** Use `make([]T, 0, expectedCap)` when you know the size. - -**Why:** 326 `make([]T, len, cap)` calls in stdlib. Avoids repeated -reallocation during append. - -**When to use:** Loops where the output size is known or estimable. - -**When NOT to use:** Unknown sizes. Small slices (<8 elements). - ---- - -## Smells - -### go:linkname Abuse - -1,711 uses in Go's own source — but actively being removed. If you -need `go:linkname`, your API boundary is wrong. - -### TODO Without Owner - -`// TODO: fix this` — unaccountable. Go's 3,428 TODOs ALL have owners. - -### Get* Methods - -Only 58 in stdlib, mostly legacy. Modern Go drops the prefix. - -### Huge Single Files - -`proc.go` is 8,156 lines. Don't copy this. The scheduler stays in one -file because splitting breaks the mental model. Your CRUD handler has -no such excuse. - -### Generated Code Without Generator - -If you check in generated code, also check in the generator or clearly -document regeneration steps. - -