9 pattern files covering stdlib (structs, interfaces, API conventions, docs, style), Kubernetes (controller/reconciler, informer/cache, leader election, code generation), comparison (stdlib vs K8s approaches), and anti-patterns. All patterns cite exact source files and line numbers.
11 KiB
API Conventions in the Go Standard Library
1. The Must Pattern
Pattern name: MustXxx (Panic on Error)
Source citation: regexp/regexp.go lines 310–320, text/template/helper.go lines 19–30
What it does: A function wraps a fallible constructor and panics if the error
is non-nil. Named MustXxx or Must (when wrapping a generic (T, error) pair).
Why: Safe initialization of package-level variables at program startup. Since
var initializers can't handle errors, Must converts programmer errors (bad
regex literals, bad templates) into immediate panics that surface during init.
Anti-pattern: Using Must in runtime code where the input is dynamic/user-provided;
panicking on recoverable errors; naming it something other than Must (e.g., PanicOnError).
Code examples from source:
// regexp/regexp.go:310-320
// MustCompile is like [Compile] but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
// text/template/helper.go:19-30
// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
// var t = template.Must(template.New("name").Parse("text"))
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}
2. Compile / MustCompile Pair
Pattern name: Fallible Constructor + Must Wrapper
Source citation: regexp/regexp.go lines 130–131, 310–320
What it does: The real constructor returns (*T, error). A parallel Must variant
wraps it for use in global variable initialization.
Why: Separates concerns: Compile is for runtime use where errors are handled;
MustCompile is for compile-time-known values where failure is a programming bug.
Anti-pattern: Only providing the Must variant (no way to handle errors gracefully); only providing the error variant (verbose for package-level vars).
Code example from source:
// regexp/regexp.go:130-131
func Compile(expr string) (*Regexp, error) {
return compile(expr, syntax.Perl, false)
}
// regexp/regexp.go:310-315
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
3. XxxWithContext Variant
Pattern name: WithContext Function Overload
Source citation: net/http/request.go lines 867–869, 894–930
What it does: Provides two function variants: NewRequest (uses context.Background())
and NewRequestWithContext (accepts an explicit context). The simple version delegates
to the context-aware one.
Why: Context was added after the original API was established. The WithContext
variant enables cancellation and deadlines; the plain variant preserves backward
compatibility and ergonomics for the common case.
Anti-pattern: Breaking the existing API signature; always requiring context even
for fire-and-forget uses; naming it NewRequestCtx.
Code example from source:
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
// net/http/request.go:894+
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// full implementation...
}
4. Nil-Opts Convention (Optional Config Pointer)
Pattern name: *Options Parameter — Nil Means Defaults
Source citation: log/slog/text_handler.go lines 28–42, log/slog/handler.go lines 135–175
What it does: A constructor accepts a pointer to an options struct. If the pointer is nil, all defaults apply. The constructor internally substitutes a zero-value struct.
Why: Keeps the simple case clean (NewTextHandler(os.Stderr, nil)) while allowing
full customization. The pointer type signals "this entire argument is optional."
Anti-pattern: Requiring a non-nil options struct even with zero customization; using variadic functional options when a simple struct suffices.
Code example from source:
// log/slog/text_handler.go:28-42
// NewTextHandler creates a [TextHandler] that writes to w,
// using the given options.
// If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
return &TextHandler{
&commonHandler{
json: false,
w: w,
opts: *opts,
mu: &sync.Mutex{},
},
}
}
5. Builder Pattern (Accumulate + Finalize)
Pattern name: Builder (Write Methods + String/Bytes Finalizer)
Source citation: strings/builder.go lines 14–113
What it does: A zero-value struct accumulates data via Write/WriteByte/WriteString methods, then produces a final result via String(). The builder is not reusable after a copyCheck-protected modification.
Why: Avoids repeated string concatenation (O(n²) allocations). The zero value is
ready to use. Implements io.Writer so it integrates with fmt.Fprintf, etc.
Anti-pattern: Allocating on every append; requiring explicit initialization;
not implementing standard interfaces (io.Writer).
Code example from source:
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
// strings/builder.go:92-96
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
// strings/builder.go:48-50
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
6. Layered API (Convenience → Full Control)
Pattern name: Convenience Wrappers over Configurable Core
Source citation: os/file.go lines 385–415
What it does: Simple functions (Open, Create) delegate to the fully configurable
OpenFile with pre-set flags. Users choose their level of control.
Why: 90% of file opens are reads or creates. Layered APIs serve the common case without hiding power. The naming makes intent clear.
Anti-pattern: Only exposing the full-power version; making users learn flag constants for simple reads; duplicating implementation across convenience functions.
Code example from source:
// os/file.go:389-393
// Open opens the named file for reading.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
// os/file.go:399-403
// Create creates or truncates the named file.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
// os/file.go:410+ (the general form)
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
// ...
}
7. Package-Level Functions Delegating to DefaultXxx
Pattern name: Convenience Package Functions
Source citation: net/http/client.go line 109, implied by http.Get, http.Post
What it does: Top-level functions like http.Get(url) call methods on the
DefaultClient. Users can bypass by creating their own Client.
Why: Makes the simple case trivial (one-liner HTTP requests). No import of constructors or setup needed. The package "just works" for basic usage.
Anti-pattern: Not providing convenience functions (forcing explicit construction even for prototyping); making the default's behavior non-obvious.
Code example from source:
// net/http/client.go:109
var DefaultClient = &Client{}
// net/http/client.go (implied pattern):
// func Get(url string) (resp *Response, err error) {
// return DefaultClient.Get(url)
// }
8. Register Pattern (Pluggable Algorithms)
Pattern name: RegisterXxx for Side-Effect Imports
Source citation: crypto/crypto.go lines 145–150
What it does: A RegisterHash(h Hash, f func() hash.Hash) function allows
algorithm implementations in sub-packages to register themselves via init().
The main package dispatches based on the registered factories.
Why: Decouples the algorithm registry from specific implementations. Users import
only the algorithms they need (e.g., _ "crypto/sha256"). Reduces binary size and
avoids circular dependencies.
Anti-pattern: Hard-coding all implementations; requiring explicit constructor calls for each algorithm; using global mutable state without clear ownership.
Code example from source:
// crypto/crypto.go:145-150
func RegisterHash(h Hash, f func() hash.Hash) {
if h == 0 || h >= maxHash {
panic("crypto: RegisterHash of unknown hash function")
}
hashes[h] = f
}
9. Graceful Shutdown Pattern
Pattern name: Close vs Shutdown (Immediate vs Graceful)
Source citation: net/http/server.go lines 3171–3220 (Close), 3221+ (Shutdown)
What it does: Provides both Close() (immediate, forceful) and Shutdown(ctx)
(graceful, waits for in-flight requests). The context on Shutdown provides a
timeout mechanism.
Why: Different operational scenarios need different termination semantics. Graceful shutdown is critical for production services; immediate close is needed for tests and emergency stops.
Anti-pattern: Only providing one shutdown mode; not accepting a context for timeout control; leaking goroutines on shutdown.
Code example from source:
// net/http/server.go:3171-3175
func (s *Server) Close() error {
s.inShutdown.Store(true)
s.mu.Lock()
defer s.mu.Unlock()
err := s.closeListenersLocked()
// ... forcefully closes all active connections
}
// net/http/server.go:3221+
// Shutdown gracefully shuts down the server without interrupting any
// active connections.
func (s *Server) Shutdown(ctx context.Context) error {
s.inShutdown.Store(true)
// ... closes listeners, waits for idle, respects ctx deadline
}
10. Channel-Based Timer/Ticker API
Pattern name: NewXxx Returning Channel-Bearing Struct
Source citation: time/tick.go lines 16–45, time/sleep.go lines 89–155
What it does: NewTicker(d) and NewTimer(d) return structs with a C <-chan Time
field. Consumers select on the channel to receive time events.
Why: Integrates time-based events with Go's concurrency primitives (select). The channel-based API composes naturally with other goroutine patterns.
Anti-pattern: Callback-based timer APIs that don't compose with select; exposing the send side of the channel; not documenting goroutine safety.
Code example from source:
// time/tick.go:16-18
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
initTicker bool
}
// time/tick.go:36-45
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic("non-positive interval for NewTicker")
}
c := make(chan Time, 1)
t := (*Ticker)(unsafe.Pointer(newTimer(when(d), int64(d), sendTime, c, syncTimer(c))))
t.C = c
return t
}