Audited all file:line citations against golang/go HEAD. 97 citations checked; 9 had drifted by 1-20 lines (off-by-one or small structural shifts in the stdlib source). Updated both inline code block comments and corresponding GitHub #L anchor links. Changes per file: patterns/api-conventions.md - strings/builder.go WriteString (92→112), String (48→46) patterns/configuration.md - crypto/tls/common.go Time field (572→575) patterns/documentation.md - net/http/server.go Handler comment (64→65), os/file.go example (17→16) patterns/structs.md - os/types.go File struct (16→15), strings/builder.go copyCheck (25→32) patterns/style.md - net/http/server.go TLSConfig (3041→3040), import block (8→9)
19 KiB
API Conventions in the Go Standard Library
Source: golang/go at commit 17bd5ab
1. The Must Pattern
Pattern name: MustXxx (Panic on Error)
Source citation: regexp/regexp.go#L310, text/template/helper.go#L19
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.
When to Use
Triggers:
- You're initializing a package-level variable with a value that is known at compile time (regex, template, URL)
- Failure means a programmer bug, not a runtime condition (the regex literal is wrong, not user input)
varinitialization can't handle the(T, error)return
Example — before:
var emailRegex *regexp.Regexp
func init() {
var err error
emailRegex, err = regexp.Compile(`^[a-z]+@[a-z]+\.[a-z]+$`)
if err != nil {
panic(err) // manual panic, verbose
}
}
Example — after:
var emailRegex = regexp.MustCompile(`^[a-z]+@[a-z]+\.[a-z]+$`)
// One line. Panics on typo (caught immediately in tests). Clean.
When NOT to Use
Don't use this when:
- The input is dynamic or user-provided (URL from a config file, regex from user input)
- You're inside a request handler or any code path where panicking would crash the server
- The error is recoverable — the caller should decide how to handle it
Over-application example:
func HandleSearch(w http.ResponseWriter, r *http.Request) {
pattern := r.URL.Query().Get("q")
re := regexp.MustCompile(pattern) // PANIC on invalid user input!
// One bad query crashes the entire server
matches := re.FindAllString(corpus, -1)
// ...
}
Better alternative:
func HandleSearch(w http.ResponseWriter, r *http.Request) {
pattern := r.URL.Query().Get("q")
re, err := regexp.Compile(pattern)
if err != nil {
http.Error(w, "invalid regex: "+err.Error(), 400)
return
}
matches := re.FindAllString(corpus, -1)
// ...
}
Why: Must is for programmer errors caught at init time, not for runtime input.
If the input can vary, the error is expected and must be handled — not panicked on.
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#L130, 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#L867, 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#L28, log/slog/handler.go#L135
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#L14
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:112-116
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
// strings/builder.go:46-48
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#L385
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.
When to Use
Triggers:
- 90% of callers need the simple case (open for read, create and truncate)
- You have a powerful function with many flags/options but most combinations are rare
- You find yourself writing the same flag combination repeatedly in calling code
Example — before:
// User must know about flags for every file open
f, err := os.OpenFile("data.json", os.O_RDONLY, 0)
// Every. Single. Time.
Example — after:
// Simple case:
f, err := os.Open("data.json") // just reads — no flags to remember
// Power case (when you actually need it):
f, err := os.OpenFile("data.json", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
When NOT to Use
Don't use this when:
- There's no clear "common case" — all callers need different flag combinations
- The convenience wrapper would hide important behavior (e.g.,
Createhides truncation — some callers are surprised) - You have 2+ equally common usage patterns that would each need their own wrapper, leading to an explosion of functions
Over-application example:
// Too many convenience wrappers — which one do I want?
func OpenForAppend(name string) (*File, error) { ... }
func OpenOrCreate(name string) (*File, error) { ... }
func OpenReadWrite(name string) (*File, error) { ... }
func OpenExclusive(name string) (*File, error) { ... }
// Users now have to remember 6 functions instead of learning 1 + flags
Better alternative:
// One convenience for the overwhelmingly common case, full-power for the rest
func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }
func Create(name string) (*File, error) { return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) }
func OpenFile(name string, flag int, perm FileMode) (*File, error) { ... }
// Only 2 convenience wrappers for the 2 dominant patterns. Everything else uses OpenFile.
Why: Layered APIs work when there's a clear 80/20 split. If you're writing a convenience wrapper for every combination, you've just created a larger API surface that's harder to navigate than the single configurable function.
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#L109, 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#L145
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#L3171 (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.
When to Use
Triggers:
- Your type manages long-lived connections or in-flight requests
- You need both "stop now" (tests, emergencies) and "drain gracefully" (deploys, SIGTERM)
- A
Close()that waits forever would make tests hang
Example — before:
type Server struct { listener net.Listener }
func (s *Server) Stop() {
s.listener.Close() // all in-flight requests get connection reset — data loss
}
Example — after:
type Server struct {
listener net.Listener
active sync.WaitGroup
}
// Immediate: drop everything
func (s *Server) Close() error {
return s.listener.Close()
}
// Graceful: stop accepting, wait for in-flight with timeout
func (s *Server) Shutdown(ctx context.Context) error {
s.listener.Close() // stop accepting new connections
done := make(chan struct{})
go func() { s.active.Wait(); close(done) }()
select {
case <-done:
return nil // all requests finished
case <-ctx.Done():
return ctx.Err() // timed out — caller decides what to do
}
}
When NOT to Use
Don't use this when:
- Your type doesn't manage long-lived resources (a pure data struct, a stateless transformer)
- Shutdown order doesn't matter — a simple
Close()suffices - You're building a CLI tool that exits the process —
os.Exitis your shutdown
Over-application example:
// Graceful shutdown for a type that holds no connections
type Calculator struct {
precision int
}
func (c *Calculator) Shutdown(ctx context.Context) error {
// ... nothing to drain, nothing to close
return nil
}
Better alternative:
// No shutdown needed — the GC handles it. Maybe a Reset() if you want to reuse.
type Calculator struct {
precision int
}
Why: The Close/Shutdown duality exists for types that own goroutines, connections, or file descriptors that outlive individual method calls. If your type is just data and methods, adding shutdown ceremony is over-engineering that confuses users into thinking there are hidden resources to manage.
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#L16, time/sleep.go#L89
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
}