docs: add configuration.md (skill test output), remove thin from-source.md
10 patterns, 989 lines. Full skill spec compliance: - Source hyperlinks (commit SHA permalinks) - Before/after code examples for every pattern - Over-application warnings with code - Anti-patterns with DON'T/DO blocks - Decision tree at end - Cross-references to related topic files Patterns: zero-value config, options struct, functional options, default instances, init-time registration, context values, builder (anti-pattern), function fields, immutable-after-use, Clone.
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
@@ -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<Type>`.
|
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
||||||
<!-- PATTERN_COMPLETE -->
|
|
||||||
Reference in New Issue
Block a user