Compare commits

...
32 Commits
Author SHA1 Message Date
Rodin 484dc7dd07 fix: update drifted file:line citations in Go patterns
Upstream golang/go has shifted several line numbers since citations
were recorded. Updated 6 citations across 3 files:

- documentation.md: server.go:55-57 → 59-62 (ErrWriteAfterFlush)
- documentation.md: transport.go:79-80 → 72-73 (Transports should be reused)
- structs.md: client.go:31-35 → 30-34 (A Client is an HTTP client)
- structs.md: client.go:59-60 → 57-58 (type Client struct)
- style.md: stream.go:280 → 292 (var _ Marshaler)
- style.md: stream.go:280-281 → 292-293 (var _ Marshaler/Unmarshaler)

Verified against golang/go HEAD (depth=1 clone).
2026-05-27 05:52:38 +00:00
Rodin 52a3629583 fix: correct drifted line citations in time/time.go and net/http/server.go
- time/time.go:928-933 -> 925-933 ("To count the number of units" block starts at 925)
- time/time.go:936-943 -> 934-942 (const Duration block starts at 934)
- net/http/server.go:3173-3174 -> 3171-3174 (Close() func starts at 3171)
2026-05-18 15:07:04 +00:00
Rodin f070fef8ee fix(citations): correct NewServeMux citation in package-design.md
server.go:2639 → 2638 (function declaration line)
Body drift: 'return new(ServeMux)' → 'return &ServeMux{}'
2026-05-11 08:04:32 -07:00
Rodin befe49257a docs: rewrite usage section as agent instructions
Frame the README for someone setting up a subagent that uses
this repo as a knowledge base. Three prompt templates:
solving problems, reviewing code, evaluating patterns.
2026-05-07 18:06:55 -07:00
Rodin a8e2020bc4 docs: add usage prompts for writing, reviewing, and evaluating 2026-05-07 18:06:07 -07:00
Rodin be394efd0b docs: define patterns vs conventions in README
Patterns are prescriptive — follow them.
Conventions are descriptive — study for ideas.

Clarifies repo purpose, directory structure, and how to
use patterns during development and review.
2026-05-07 18:04:50 -07:00
Rodin 65a433d0c6 chore: merge golang-conventions and prometheus-conventions into sources/
Absorbed content from rodin/golang-conventions and
rodin/prometheus-conventions into a sources/ directory.
Reference material — descriptive, not prescriptive.

Part of taxonomy cleanup (elixir-patterns issue #4).
2026-05-07 18:02:04 -07:00
Rodin 0de5f54365 fix: correct drifted citation for tls.Config.Clone
crypto/tls/common.go#L925 was wrong; the Clone() method is at L996
in commit 17bd5ab8c650155dd2bd09f7005726552639eea0.

Audited all citations across all pattern .md files — only this one
had drifted. All others verified OK against the pinned golang/go commit.
2026-05-06 17:21:18 -07:00
Rodin 52503d24c2 fix: update drifted file:line citations to match current golang/go source
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)
2026-05-06 17:17:20 -07:00
Rodin 7bcc1cc62b docs: rewrite anti-patterns as general Go smells (was Kubernetes-only)
Previous version was entirely Kubernetes controller-specific (from
kubernetes-conventions extraction). Replaced with 10 general Go
anti-patterns from studying golang/go stdlib.

Patterns: error+value returns, large interfaces, init() abuse,
stuttering names, naked returns, error formatting, channel misuse,
returning interfaces, panic in libraries, interface placement.
2026-04-30 16:03:43 -07:00
Rodin ffcc0fccf3 chore: remove leftover tooling artifacts (watermark, changelog) 2026-04-30 15:49:25 -07:00
Rodin d73c81dab1 Add common-mistakes.md: 10 Go code smells from other languages
Covers nil-check-after-use, goroutine leaks, interface pollution,
stuttering names, init() abuse, ignored errors, interface-returning
constructors, mutex value copying, channel misuse, and premature
abstraction. Each entry includes BAD/GOOD examples, trigger conditions,
and exceptions.
2026-04-30 15:46:03 -07:00
Rodin c8ed244a07 feat: add source hyperlinks (commit SHA permalinks) to all pattern files
Every source reference now links to the exact line in the golang/go
repo at commit 17bd5ab. Added PATTERN_COMPLETE sentinels.

Total: 154 hyperlinks across 10 topic files.
2026-04-30 14:42:20 -07:00
Rodin 99c0865e93 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.
2026-04-30 14:26:31 -07:00
Rodin c7e61565c0 docs: full iterative patterns extraction from golang/go
794 lines, 35+ patterns across 9 topics with hyperlinked sources.
Includes frequency data from the source (281 interfaces, 55 sentinels,
262 constructors, 309 context-accepting functions, 2685 t.Helper calls).

Topics: interfaces, errors, testing, packages, concurrency,
documentation, naming, configuration, extension, performance, smells.

All examples are real code from the Go source, not invented.
2026-04-30 13:45:02 -07:00
Rodin dfe03e0675 fix: add 'When to use' to every pattern (was missing) 2026-04-30 13:29:50 -07:00
Rodin 65cae45f13 docs: add patterns extracted from golang/go source
Using codebase-analysis skill (patterns mode) on the language source.
Real examples from the repo, not invented. Each pattern has:
- Rule, Example, Why, When NOT to use, Source file.

Topics: interface design, error handling, testing, package org,
concurrency, documentation, naming, smells.
2026-04-30 13:26:29 -07:00
Rodin fe74d5d47c chore: remove project conventions from sources/
These have been promoted to standalone repos:
- rodin/cockroachdb-conventions
- rodin/prometheus-conventions
- rodin/temporal-conventions
2026-04-30 11:45:45 -07:00
Rodin 498a3e9b27 docs: add Temporal patterns (9 patterns from temporalio/temporal)
Key patterns:
- Effect buffer (transactional side effects with rollback)
- Soft assertions (log invariant violations, don't crash)
- Type-safe state transitions (HSM with source validation)
- Mutable vs immutable context (type-level access control)
- Goroutine Handle (safe lifecycle, predates CockroachDB by 3.5y)
- Dynamic config with generics (566 settings, namespace-scoped)
- Composable predicates (filter algebra with flattening)
- Persistence plugin registration (init pattern)
- ShutdownOnce (CAS-based safe channel close)
2026-04-30 11:40:31 -07:00
Rodin 2f7536766c chore: move cross-ecosystem analysis to patterns-vs-guidelines
These docs analyze multiple ecosystems (Go + Elixir) and
don't belong in a single-ecosystem patterns repo.
2026-04-30 10:50:36 -07:00
Rodin 4185747da6 docs: testing philosophy + API evolution strategies
Four testing models: defense-in-depth (CockroachDB), golden
files (Prometheus), fake adapters (Ecto), testing modes (Oban).

Three evolution strategies: version gates (distributed),
numbered migrations (schema), compile-time deprecation (library).
2026-04-30 10:33:55 -07:00
Rodin d6f36b67c8 docs: cross-cutting concerns analysis (logging, config, retry, lifecycle)
How CockroachDB, Prometheus, Ecto, and Oban handle the
things that touch everything but belong nowhere. Includes
red flags and review questions for each concern.
2026-04-30 10:31:19 -07:00
Rodin cee58e85a4 docs: ecosystem-level analysis — how codebases present to consumers
Extension points, deliberate absences, test architecture,
and consumer contracts across CockroachDB, Prometheus, Ecto, Oban.

Key insight: smaller interface → larger ecosystem.
2026-04-30 10:02:07 -07:00
Rodin 725308c37a docs: architectural analysis across CockroachDB, Prometheus, Ecto, Oban
Not just per-file patterns — structural analysis of how these
codebases organize at scale. Key findings:
- 116 packages @ 4 files each (CockroachDB)
- Interface layer breaks circular deps
- Testability designed in, not bolted on
- Composition via data, not inheritance
2026-04-30 09:28:03 -07:00
Rodin 758ae5dae4 docs: add patterns extracted from cockroachdb and prometheus
CockroachDB: 4 patterns (Stopper lifecycle, leak detection, two-phase shutdown, CloserFn adapter)
Prometheus: 5 patterns (atomic file ops, DefaultOptions, aligned timestamps, sentinel errors, compile-time interface checks)
2026-04-30 09:04:11 -07:00
Rodin 1ef2a4a189 changelog: 2026-04-30 digest 2026-04-30 14:07:37 +00:00
aweiker 733aa7d261 docs: add when-not to style + smells (package-design + documentation already done) 2026-04-30 13:31:47 +00:00
aweiker 11048ae73e docs: add when-not to interfaces + error-handling + concurrency 2026-04-30 13:26:20 +00:00
aweiker a7a853bb43 docs: add when-not to structs + testing-advanced + api-conventions 2026-04-30 13:24:01 +00:00
aweiker 631be02392 refactor: remove Kubernetes content (moved to rodin/kubernetes-patterns) 2026-04-30 12:10:11 +00:00
aweiker eb9171368b docs: add 'when to use' triggers + examples to all patterns
Added 'When to Use' subsections with concrete decision triggers and
before/after Go code examples to patterns across all directories:

- patterns/error-handling.md (3 patterns: sentinels, wrapping, Join)
- patterns/concurrency.md (4 patterns: Mutex, Once, done channels, pipelines)
- patterns/interfaces.md (4 patterns: small interfaces, accept/return, adapter, optional)
- patterns/structs.md (3 patterns: zero-value, constructors, config structs)
- patterns/package-design.md (3 patterns: internal/, init(), context keys)
- patterns/style.md (3 patterns: interface checks, iota constants, named types)
- patterns/testing-advanced.md (3 patterns: table tests, golden files, httptest)
- patterns/api-conventions.md (3 patterns: Must, layered API, graceful shutdown)
- patterns/documentation.md (2 patterns: examples, deprecated)
- kubernetes/patterns.md (3 patterns: controller, workqueue, leader election)
- kubernetes/production-go.md (2 patterns: codegen, HandleCrash)
- smells/anti-patterns.md (2 anti-patterns: cache mutation, edge-triggered)
2026-04-30 12:08:41 +00:00
rodin 0e5974f39a add MIT license 2026-04-30 11:58:36 +00:00
22 changed files with 5092 additions and 1456 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Aaron Weiker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+50 -9
View File
@@ -1,17 +1,58 @@
# Go Patterns # Go Patterns
Idiomatic Go patterns extracted from the [Go standard library](https://github.com/golang/go) and [Kubernetes](https://github.com/kubernetes/kubernetes) source code with verified file:line citations. **Prescriptive.** Follow these when writing Go code.
A pattern is a reusable solution to a recurring problem. Each one has:
- **When to use** — the problem it solves
- **When NOT to use** — where it causes harm
- **Why** — the reasoning, not just the rule
- **Source citations** — verified file:line from real codebases
These are derived from what mature Go codebases *actually do*, not opinions or blog posts.
## Structure ## Structure
- `patterns/` — Go stdlib patterns (interfaces, errors, concurrency, structs, testing, docs, style, API conventions, packages) - `patterns/` — what to do (interfaces, errors, concurrency, testing, packages, etc.)
- `kubernetes/` — Production-scale patterns from Kubernetes (controllers, informers, workqueues) - `smells/` — what NOT to do (anti-patterns, common mistakes)
- `comparison/` — stdlib vs Kubernetes patterns - `sources/` — reference material from specific projects (golang/go, Prometheus). Study for ideas, don't copy blindly.
- `smells/` — Anti-patterns and common Go mistakes
- `changelog/` — Daily digest of merged PRs
## Philosophy ## How to use
These rules are derived from what the Go source code actually does, not opinions or blog posts. Every pattern cites specific files and line numbers. Give your agent these instructions depending on the task:
When unsure how to do something in Go, look at how the standard library does it. ### Solving a problem
> You have access to a patterns repo containing proven solutions to recurring Go problems. When I describe a problem:
>
> 1. Identify which pattern files are relevant (read them)
> 2. Check if my problem matches a "When to use" case
> 3. Check if it matches a "When NOT to use" case
> 4. If a pattern fits: suggest the approach, cite the pattern, explain why it applies here
> 5. If no pattern fits: say so, and suggest an approach grounded in the principles you see across the patterns
> 6. If my problem matches a smell: warn me before I make the mistake
>
> Never suggest something that contradicts a documented pattern without explicitly calling out the deviation and justifying it.
### Reviewing code
> You have access to a patterns repo that defines how Go code should be written. For each file in the diff:
>
> 1. Read the relevant pattern files
> 2. Verify the code follows the documented patterns
> 3. If it deviates: flag it with a reference to the specific pattern, section, and why it matters
> 4. If it matches a smell: flag it as a known anti-pattern
> 5. A deviation without justification is a finding
>
> Don't invent rules. Only flag what the patterns document.
### Evaluating a pattern
> Read the pattern file. Compare against how the following projects handle the same problem: [list projects]. Does the pattern hold? Are there cases where it breaks down? Should it be updated, split, or retired? File your findings as an issue.
## Patterns vs Conventions
**Pattern** = prescriptive. "When you face X, do Y." Language-scoped. Follow these.
**Convention** = descriptive. "Project Z does it this way." Context-specific. Study for ideas — applying another project's conventions to yours without understanding their constraints causes harm.
The `sources/` directory is convention material absorbed from thin repos. The `patterns/` directory is what you actually follow.
View File
-325
View File
@@ -1,325 +0,0 @@
# Stdlib vs Kubernetes: Where K8s Does Things Differently
## 1. Concurrency: Channels vs. Condition Variables + Sets
### Stdlib approach
Go stdlib idiom: communicate via channels. A worker pool typically uses a `chan T` for work items.
```go
// Typical stdlib pattern:
jobs := make(chan Job, 100)
for i := 0; i < workers; i++ {
go func() {
for job := range jobs {
process(job)
}
}()
}
```
### Kubernetes approach
The workqueue uses `sync.Cond` + `sets.Set` instead of channels.
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 190–300)
```go
type Typed[t comparable] struct {
queue Queue[t]
dirty sets.Set[t] // Items needing processing
processing sets.Set[t] // Items currently being processed
cond *sync.Cond // Notification mechanism
}
```
### Why K8s is different
Channels don't deduplicate. If you send the same key twice to a channel, it gets processed twice. K8s needs:
- **Deduplication**: same object modified 5 times → process once with latest state
- **Re-entrant marking**: if modified during processing, re-queue after Done()
- **Inspection**: can query queue length, processing count for metrics
A channel-based design would require a separate dedup layer anyway, losing its simplicity advantage.
---
## 2. Error Handling: Single Errors vs. Error Aggregation
### Stdlib approach
Return a single error. Wrap with `fmt.Errorf("...: %w", err)`.
### Kubernetes approach
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/errors/errors.go` (lines 34–100)
```go
type Aggregate interface {
error
Errors() []error
Is(error) bool
}
func NewAggregate(errlist []error) Aggregate {
// Filters nils, deduplicates messages
}
```
### Why K8s is different
A controller sync often does multiple operations (create 3 pods, update 2 services). You want to attempt all of them, not fail fast on the first error. Error aggregation collects all failures so the user sees the full picture.
**Also**: the `Aggregate` interface properly implements `errors.Is()` by checking if *any* contained error matches — which `errors.Join` didn't originally support well.
---
## 3. Retry: stdlib has nothing; K8s has structured retry
### Stdlib approach
There's no retry utility in stdlib. You write your own loop.
### Kubernetes approach
**Source:** `staging/src/k8s.io/client-go/util/retry/util.go` (lines 30–100)
```go
var DefaultRetry = wait.Backoff{
Steps: 5,
Duration: 10 * time.Millisecond,
Factor: 1.0,
Jitter: 0.1,
}
func RetryOnConflict(backoff wait.Backoff, fn func() error) error {
// Retries only on HTTP 409 Conflict
return OnError(backoff, errors.IsConflict, fn)
}
```
### Why K8s is different
In a distributed system, retries are *the* primary reliability mechanism. Stdlib doesn't provide them because stdlib targets single-machine programs. Kubernetes needs:
- Configurable backoff (steps, factor, jitter, cap)
- Condition-based retry (retry only on specific error types)
- Context-aware cancellation
---
## 4. Polling: time.Ticker vs. Contextual Loop With Crash Protection
### Stdlib approach
```go
ticker := time.NewTicker(interval)
defer ticker.Stop()
for range ticker.C {
doWork()
}
```
### Kubernetes approach
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/wait/backoff.go` (lines 240–260), `loop.go` (lines 38–80)
```go
func BackoffUntilWithContext(ctx context.Context, f func(ctx context.Context), backoff BackoffManager, sliding bool) {
for {
select {
case <-ctx.Done():
return
default:
}
if !sliding { t = backoff.Backoff() }
func() {
defer runtime.HandleCrashWithContext(ctx)
f(ctx)
}()
if sliding { t = backoff.Backoff() }
// ... wait for timer or context cancellation
}
}
```
### Why K8s is different
- **Crash protection**: a panic in `doWork()` shouldn't kill the whole process
- **Sliding vs non-sliding**: controls whether interval includes execution time
- **Context cancellation**: allows clean shutdown
- **Jitter**: prevents thundering herd when many controllers sync at similar intervals
- **Double-check for cancellation**: Go's select is non-deterministic, so short timers can "win" against a cancelled context
---
## 5. Graceful Shutdown: http.Server.Shutdown vs. Multi-Phase Orchestration
### Stdlib approach (net/http)
**Source:** `/tmp/go-src/src/net/http/server.go` (lines 3221–3260)
```go
func (s *Server) Shutdown(ctx context.Context) error {
s.inShutdown.Store(true)
s.closeListenersLocked()
// Poll for idle connections
for {
if s.closeIdleConns() { return nil }
select {
case <-ctx.Done(): return ctx.Err()
case <-timer.C: timer.Reset(nextPollInterval())
}
}
}
```
### Kubernetes approach
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 171–196)
```go
func (dc *DeploymentController) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash()
dc.eventBroadcaster.StartStructuredLogging(3)
dc.eventBroadcaster.StartRecordingToSink(...)
defer dc.eventBroadcaster.Shutdown()
var wg sync.WaitGroup
defer func() {
dc.queue.ShutDown() // Stop accepting new work
wg.Wait() // Wait for workers to finish
}()
// Gate: don't start until caches are synced
if !cache.WaitForNamedCacheSyncWithContext(ctx, ...) { return }
for i := 0; i < workers; i++ {
wg.Go(func() {
wait.UntilWithContext(ctx, dc.worker, time.Second)
})
}
<-ctx.Done() // Block until context cancelled
}
```
### Why K8s is different
Stdlib's shutdown is **reactive** (wait for connections to drain). Kubernetes' shutdown is **multi-phase orchestrated**:
1. Stop accepting new events (close watch connections)
2. Drain the work queue (process remaining items)
3. Wait for in-flight syncs to complete
4. Shut down event recorders
The queue's `ShutDownWithDrain()` is the K8s-specific innovation: it waits until all in-flight items call `Done()`.
---
## 6. Type Systems: interfaces vs. Runtime Type Registry
### Stdlib approach
Interfaces for polymorphism. If you need to serialize, use `encoding/json` with struct tags.
### Kubernetes approach
A full runtime type registry (Scheme) that maps between GVK strings and Go types.
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go`
### Why K8s is different
Stdlib's `encoding/json` requires knowing the concrete type at compile time. Kubernetes must:
- Deserialize objects from the network without knowing their type in advance
- Convert between API versions (`v1beta1.Deployment` → `v1.Deployment`)
- Support third-party types (CRDs) that don't exist at compile time
- Apply defaulting and validation based on type metadata
This forces a **runtime type system layered on top of Go's static types**.
---
## 7. Testing: httptest vs. Fake Clients + Reactors
### Stdlib approach
`net/http/httptest` provides a test server. You make real HTTP calls against it.
### Kubernetes approach
Fake clientsets with reactor chains:
```go
// Generated fake clients intercept API calls
fakeClient := fake.NewSimpleClientset(existingObjects...)
fakeClient.PrependReactor("create", "pods", func(action testing.Action) (bool, runtime.Object, error) {
// Custom test behavior
return true, nil, fmt.Errorf("simulated error")
})
```
### Why K8s is different
Testing a controller doesn't require a running API server. The fake client + informer pattern lets you:
- Inject specific starting states
- Simulate failures at specific operations
- Run synchronously (no network delay)
- Test the controller logic in isolation
---
## 8. Lifecycle: main() returns vs. Infinite Reconciliation
### Stdlib pattern
Programs start, do work, return.
### Kubernetes pattern
Controllers start and run *forever*, continuously reconciling.
```go
// The fundamental difference: stdlib programs terminate, controllers don't
func main() {
// Stdlib: do work, exit
result := compute()
fmt.Println(result)
}
// Kubernetes: infinite loop with eventual consistency
func (c *Controller) Run(ctx context.Context) {
// Run forever until context cancelled
for i := 0; i < workers; i++ {
go wait.UntilWithContext(ctx, c.worker, time.Second)
}
<-ctx.Done()
}
```
### Why K8s is different
The real world is adversarial. Networks fail, nodes die, humans make mistakes. A one-shot program can't handle drift. The reconciliation loop is Kubernetes' answer to the CAP theorem: you can't guarantee consistency in a single call, but you can achieve it *eventually* through repetition.
---
## 9. Shared State: Package-Level vs. Shared Informer Cache
### Stdlib approach
Package-level variables, or pass state through function parameters.
### Kubernetes approach
The SharedInformerFactory creates a single in-memory cache per resource type, shared by all controllers in the process.
```go
// All controllers share ONE watch and ONE cache per resource:
informerFactory := informers.NewSharedInformerFactory(client, resyncPeriod)
deployInformer := informerFactory.Apps().V1().Deployments()
// Controller A and B both get events from the same informer
deployInformer.Informer().AddEventHandler(controllerA)
deployInformer.Informer().AddEventHandler(controllerB)
```
### Why K8s is different
Without sharing:
- 20 controllers × watch for Pods = 20 TCP connections to API server
- 20 copies of all Pods in memory
With SharedInformerFactory:
- 1 TCP connection for Pods
- 1 copy in memory
- Events fanned out to all registered handlers
---
## 10. Configuration: Flags/Env vs. Feature Gates
### Stdlib approach
`flag` package, environment variables, config files.
### Kubernetes approach
Feature gates: a versioned, lifecycle-aware configuration system.
### Why K8s is different
Stdlib's flag package is for a single binary. Kubernetes has:
- Hundreds of features in various stages of maturity
- Features that must be consistent across control plane components
- Features that need to be enabled/disabled without redeployment
- Features with dependencies on other features
- Automated testing that exercises all combinations
Feature gates encode *maturity* (alpha/beta/GA) alongside the boolean value, something `flag.Bool` can never express.
-405
View File
@@ -1,405 +0,0 @@
# Kubernetes-Specific Patterns
## 1. Controller / Reconciler Pattern
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 65–530)
### What it does
The controller pattern is the central design pattern of Kubernetes. Every controller watches a set of resources, maintains a work queue, and reconciles desired state with actual state through a sync loop.
### Why
Distributed systems can't guarantee that a single API call will bring the world to desired state. The controller pattern provides eventual consistency by continuously reconciling — it handles missed events, partial failures, and concurrent modifications.
### Structure
```go
// pkg/controller/deployment/deployment_controller.go:65-95
type DeploymentController struct {
rsControl controller.RSControlInterface
client clientset.Interface
// Testability: sync handler is injectable
syncHandler func(ctx context.Context, dKey string) error
enqueueDeployment func(deployment *apps.Deployment)
// Listers: read from local cache, not API server
dLister appslisters.DeploymentLister
rsLister appslisters.ReplicaSetLister
podLister corelisters.PodLister
// Synced funcs: gate processing until caches are warm
dListerSynced cache.InformerSynced
rsListerSynced cache.InformerSynced
// Work queue: rate-limited, deduplicating
queue workqueue.TypedRateLimitingInterface[string]
}
```
### The Canonical Worker Loop
```go
// pkg/controller/deployment/deployment_controller.go:481-515
func (dc *DeploymentController) worker(ctx context.Context) {
for dc.processNextWorkItem(ctx) {
}
}
func (dc *DeploymentController) processNextWorkItem(ctx context.Context) bool {
key, quit := dc.queue.Get()
if quit {
return false
}
defer dc.queue.Done(key)
err := dc.syncHandler(ctx, key)
dc.handleErr(ctx, err, key)
return true
}
func (dc *DeploymentController) handleErr(ctx context.Context, err error, key string) {
if err == nil || errors.HasStatusCause(err, v1.NamespaceTerminatingCause) {
dc.queue.Forget(key) // Success: clear rate limiter
return
}
if dc.queue.NumRequeues(key) < maxRetries {
dc.queue.AddRateLimited(key) // Retry with backoff
return
}
utilruntime.HandleError(err)
dc.queue.Forget(key) // Give up after maxRetries
}
```
### Key Properties
1. **Level-triggered, not edge-triggered** — the sync loop reads current state, not diffs
2. **Idempotent** — running sync twice produces the same result
3. **Key-based deduplication** — the workqueue coalesces multiple events for the same object
4. **Bounded retries** — exponential backoff with a max retry count (15 retries = ~82s max delay)
---
## 2. Informer + Cache + Workqueue Combo
**Source:** `staging/src/k8s.io/client-go/tools/cache/shared_informer.go` (lines 144–283), `staging/src/k8s.io/client-go/informers/factory.go` (lines 57–250)
### What it does
The Informer provides a local read cache of API server state, backed by a List+Watch connection. The SharedInformerFactory ensures only one informer per resource type exists per process, preventing duplicate watches.
### Why
- **Reduces API server load**: controllers read from local cache (Listers) instead of hitting the API
- **Reduces latency**: events are delivered via callbacks, no polling
- **Memory efficiency**: shared informers prevent N controllers from opening N watches
### Architecture
```
API Server
│
├── List (initial sync)
│
└── Watch (streaming updates)
│
▼
SharedIndexInformer
├── local Store (thread-safe cache)
├── Indexer (secondary indexes)
└── Event Handlers → [Controller1, Controller2, ...]
│
▼
WorkQueue
│
▼
worker goroutines
```
### SharedInformerFactory Pattern
```go
// staging/src/k8s.io/client-go/informers/factory.go:57-77
type sharedInformerFactory struct {
client kubernetes.Interface
lock sync.Mutex
informers map[reflect.Type]cache.SharedIndexInformer
startedInformers map[reflect.Type]bool
wg sync.WaitGroup
shuttingDown bool
}
```
### Registration via Event Handlers
```go
// pkg/controller/deployment/deployment_controller.go:117-146
dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) { dc.addDeployment(logger, obj) },
UpdateFunc: func(oldObj, newObj interface{}) { dc.updateDeployment(logger, oldObj, newObj) },
DeleteFunc: func(obj interface{}) { dc.deleteDeployment(logger, obj) },
})
```
### Cache Sync Gate
```go
// pkg/controller/deployment/deployment_controller.go:189
if !cache.WaitForNamedCacheSyncWithContext(ctx, dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) {
return
}
```
---
## 3. Workqueue: Typed Rate-Limiting Queue
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 33–370), `rate_limiting_queue.go`
### What it does
A concurrent-safe work queue with three critical properties:
1. **Deduplication** (dirty set) — same item added twice results in one processing
2. **Re-entrancy** (processing set) — if an item is added while being processed, it's re-queued after Done()
3. **Rate limiting** — exponential backoff on failures
### Why
In a controller, multiple events may fire for the same object in rapid succession. Without deduplication, you'd process stale intermediate states. The dirty/processing set design ensures you always process the latest state while never losing notifications.
### The Dirty/Processing Dance
```go
// staging/src/k8s.io/client-go/util/workqueue/queue.go:227-252
func (q *Typed[T]) Add(item T) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.shuttingDown { return }
if q.dirty.Has(item) {
if !q.processing.Has(item) {
q.queue.Touch(item) // Allow priority changes
}
return // Already marked for processing
}
q.dirty.Insert(item)
if q.processing.Has(item) {
return // Being processed, will re-queue on Done()
}
q.queue.Push(item)
q.cond.Signal()
}
func (q *Typed[T]) Done(item T) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
q.processing.Delete(item)
if q.dirty.Has(item) {
q.queue.Push(item) // Was modified during processing
q.cond.Signal()
}
}
```
### Rate-Limited Requeue
```go
// staging/src/k8s.io/client-go/util/workqueue/rate_limiting_queue.go:120-122
func (q *rateLimitingType[T]) AddRateLimited(item T) {
q.TypedDelayingInterface.AddAfter(item, q.rateLimiter.When(item))
}
```
---
## 4. Tombstone Pattern (DeletedFinalStateUnknown)
**Source:** `staging/src/k8s.io/client-go/tools/cache/delta_fifo.go` (lines 797–801)
### What it does
When a watch disconnects and reconnects, some delete events may be missed. The DeltaFIFO synthesizes a `DeletedFinalStateUnknown` ("tombstone") containing the last known state of the object.
### Why
Without this, controllers would never learn about deletions that happened during disconnects.
```go
// staging/src/k8s.io/client-go/tools/cache/delta_fifo.go:797-801
type DeletedFinalStateUnknown struct {
Key string
Obj interface{}
}
```
### How controllers handle it
```go
// pkg/controller/deployment/deployment_controller.go:210-224
func (dc *DeploymentController) deleteDeployment(logger klog.Logger, obj interface{}) {
d, ok := obj.(*apps.Deployment)
if !ok {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown)
if !ok {
utilruntime.HandleError(fmt.Errorf("couldn't get object from tombstone %#v", obj))
return
}
d, ok = tombstone.Obj.(*apps.Deployment)
if !ok {
utilruntime.HandleError(fmt.Errorf("tombstone contained object that is not a Deployment %#v", obj))
return
}
}
dc.enqueueDeployment(d)
}
```
---
## 5. Controller Expectations Pattern
**Source:** `pkg/controller/controller_utils.go` (lines 130–315)
### What it does
Expectations track pending creates/deletes to prevent controllers from taking action on stale cache state. A controller won't sync until its expectations are satisfied or expired.
### Why
Between a controller issuing a create and the informer cache reflecting that new object, there's a window where the controller might create duplicates. Expectations close this gap.
```go
// pkg/controller/controller_utils.go:153-173
type ControllerExpectationsInterface interface {
GetExpectations(controllerKey string) (*ControlleeExpectations, bool, error)
SatisfiedExpectations(logger klog.Logger, controllerKey string) bool
DeleteExpectations(logger klog.Logger, controllerKey string)
SetExpectations(logger klog.Logger, controllerKey string, add, del int) error
ExpectCreations(logger klog.Logger, controllerKey string, adds int) error
ExpectDeletions(logger klog.Logger, controllerKey string, dels int) error
CreationObserved(logger klog.Logger, controllerKey string)
DeletionObserved(logger klog.Logger, controllerKey string)
}
```
---
## 6. OwnerReference / Controller Ref Manager Pattern
**Source:** `pkg/controller/controller_ref_manager.go` (lines 37–80)
### What it does
Implements garbage collection ownership through OwnerReferences. The ControllerRefManager handles adopting orphaned resources and releasing resources that no longer match.
### Why
Multiple controllers may create children. The ownership model ensures exactly one controller owns each child, enabling garbage collection and preventing conflicts.
```go
// pkg/controller/controller_ref_manager.go:37-50
type BaseControllerRefManager struct {
Controller metav1.Object
Selector labels.Selector
canAdoptErr error
canAdoptOnce sync.Once // Lazy, one-shot adoption check
CanAdoptFunc func(ctx context.Context) error
}
// The claim logic: adopt if matching and orphaned, release if owned but not matching
func (m *BaseControllerRefManager) ClaimObject(ctx context.Context, obj metav1.Object,
match func(metav1.Object) bool,
adopt, release func(context.Context, metav1.Object) error) (bool, error) {
controllerRef := metav1.GetControllerOfNoCopy(obj)
if controllerRef != nil {
if controllerRef.UID != m.Controller.GetUID() {
return false, nil // Owned by someone else
}
if match(obj) {
return true, nil // Already ours and matches
}
// Ours but no longer matches → release
}
// Orphan → adopt if matches
}
```
---
## 7. Leader Election Pattern
**Source:** `staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go` (lines 116–230)
### What it does
Provides distributed mutex semantics using a Kubernetes resource (Lease) as the lock. Only one instance of a controller runs actively; others are hot standbys.
### Why
Controller-manager runs multiple replicas for HA. Only one should reconcile to avoid conflicts.
```go
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go:116-163
type LeaderElectionConfig struct {
Lock rl.Interface
LeaseDuration time.Duration // Default 15s — how long a lease is valid
RenewDeadline time.Duration // Default 10s — how long leader retries renewal
RetryPeriod time.Duration // Default 2s — how often candidates check
Callbacks LeaderCallbacks
ReleaseOnCancel bool
}
type LeaderCallbacks struct {
OnStartedLeading func(context.Context)
OnStoppedLeading func()
OnNewLeader func(identity string)
}
// staging/src/k8s.io/client-go/tools/leaderelection/leaderelection.go:211-226
func (le *LeaderElector) Run(ctx context.Context) {
defer runtime.HandleCrashWithContext(ctx)
defer le.config.Callbacks.OnStoppedLeading()
if !le.acquire(ctx) {
return
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
go le.config.Callbacks.OnStartedLeading(ctx)
le.renew(ctx)
}
```
---
## 8. Feature Gates Pattern
**Source:** `pkg/features/kube_features.go` (lines 34–2811), `staging/src/k8s.io/client-go/features/features.go` (lines 34–80)
### What it does
A global registry of boolean flags that control feature rollout. Features progress through Alpha → Beta → GA → Deprecated lifecycle stages.
### Why
Kubernetes serves thousands of clusters. Features must be safe to enable/disable at runtime across versions. Feature gates provide:
- Progressive rollout (alpha off by default, beta on, GA locked)
- Per-version semantics (a feature may become beta in v1.28)
- Testing isolation
```go
// staging/src/k8s.io/client-go/features/features.go:50-70
type Feature string
type FeatureSpec struct {
Default bool
LockToDefault bool
PreRelease prerelease
Version *version.Version
}
type Gates interface {
Enabled(key Feature) bool
}
// pkg/features/kube_features.go:50-58 (example feature definition)
const (
AllowDNSOnlyNodeCSR featuregate.Feature = "AllowDNSOnlyNodeCSR"
// ... 2700+ lines of feature definitions
)
```
### Registration at init()
```go
// pkg/features/kube_features.go:2798-2811
func init() {
ca := &clientAdapter{utilfeature.DefaultMutableFeatureGate}
runtime.Must(clientfeatures.AddVersionedFeaturesToExistingFeatureGates(ca))
clientfeatures.ReplaceFeatureGates(ca)
runtime.Must(utilfeature.DefaultMutableFeatureGate.AddVersioned(defaultVersionedKubernetesFeatureGates))
}
```
-369
View File
@@ -1,369 +0,0 @@
# Production Go Patterns (from Kubernetes)
Patterns for building large-scale Go codebases that go beyond what stdlib teaches you.
## 1. Code Generation Pattern
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/zz_generated.deepcopy.go`, `staging/src/k8s.io/client-go/informers/apps/v1/deployment.go`
### What it does
Kubernetes generates massive amounts of boilerplate code from annotations on types:
- `deepcopy-gen` → DeepCopy/DeepCopyInto methods
- `informer-gen` → typed informers (List/Watch/Lister per resource)
- `client-gen` → typed client sets
- `lister-gen` → typed lister interfaces
- `conversion-gen` → version conversion functions
- `defaulter-gen` → defaulting functions
### Why
At Kubernetes scale (~50 resource types × multiple versions), hand-writing deep copy, client wrappers, and conversion code is:
1. Error-prone (forgetting to copy a new field breaks everything)
2. Unmaintainable (thousands of nearly-identical files)
3. Not verifiable by human review
### How it works
Annotations drive generation:
```go
// +k8s:deepcopy-gen=true
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type RawExtension struct { ... }
```
Generated output uses `zz_generated.` prefix (convention for "don't edit"):
```go
// staging/src/k8s.io/apimachinery/pkg/runtime/zz_generated.deepcopy.go:22
// Code generated by deepcopy-gen. DO NOT EDIT.
package runtime
func (in *RawExtension) DeepCopyInto(out *RawExtension) {
*out = *in
if in.Raw != nil {
in, out := &in.Raw, &out.Raw
*out = make([]byte, len(*in))
copy(*out, *in)
}
}
```
Generated informers (note the header comment):
```go
// staging/src/k8s.io/client-go/informers/apps/v1/deployment.go:20
// Code generated by informer-gen. DO NOT EDIT.
```
### Key Insight
**Stdlib has no code generation culture.** stdlib keeps things small enough that hand-writing works. Kubernetes proves that once you cross ~20 types with shared behavior, code gen is the only sane path.
---
## 2. The Scheme / Type Registry Pattern
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go` (lines 38–100), `scheme_builder.go`
### What it does
The Scheme is a runtime type registry that maps:
- `GroupVersionKind` → Go type (`reflect.Type`)
- Go type → `[]GroupVersionKind`
- Provides serialization, defaulting, conversion, and validation dispatch
### Why
Kubernetes has 50+ resource types across 15+ API groups, each with multiple versions. The Scheme provides:
- **Dynamic dispatch**: serialize any Object without knowing its concrete type
- **Version conversion**: convert between v1 and v1beta1 transparently
- **Pluggability**: third-party resources register into the same system
### Structure
```go
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go:38-98
type Scheme struct {
gvkToType map[schema.GroupVersionKind]reflect.Type
typeToGVK map[reflect.Type][]schema.GroupVersionKind
unversionedTypes map[reflect.Type]schema.GroupVersionKind
defaulterFuncs map[reflect.Type]func(interface{})
validationFuncs map[reflect.Type]func(ctx, op, obj, oldObj) field.ErrorList
converter *conversion.Converter
versionPriority map[string][]string
}
```
### SchemeBuilder Pattern
```go
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme_builder.go:23-48
type SchemeBuilder []func(*Scheme) error
func (sb *SchemeBuilder) AddToScheme(s *Scheme) error {
for _, f := range *sb {
if err := f(s); err != nil {
return err
}
}
return nil
}
func (sb *SchemeBuilder) Register(funcs ...func(*Scheme) error) {
*sb = append(*sb, f)
}
```
### How Registration Works
```go
// staging/src/k8s.io/apimachinery/pkg/runtime/scheme.go:151-160
func (s *Scheme) AddKnownTypes(gv schema.GroupVersion, types ...Object) {
for _, obj := range types {
t := reflect.TypeOf(obj)
if t.Kind() != reflect.Pointer {
panic("All types must be pointers to structs.")
}
t = t.Elem()
s.AddKnownTypeWithName(gv.WithKind(t.Name()), obj)
}
}
```
### Key Insight
This is Java's ServiceLoader / dependency injection adapted for Go's type system. Stdlib uses interfaces; Kubernetes needs a **runtime type system on top of Go's static type system** because API objects must be dynamically dispatched across version boundaries.
---
## 3. The runtime.Object Interface
**Source:** `staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go` (lines 333–342)
### What it does
Every Kubernetes API object must implement this two-method interface:
```go
// staging/src/k8s.io/apimachinery/pkg/runtime/interfaces.go:337-341
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
```
### Why
- `GetObjectKind()` — allows the serialization layer to determine what type an object is without reflection
- `DeepCopyObject()` — enables safe concurrent access (informer cache is shared; mutations must happen on copies)
### Key Insight
**This is the foundation of Kubernetes' extensibility.** Any Go struct that satisfies these two methods can participate in the entire API machinery — serialization, storage, admission, informers, etc. CRDs generate code that implements this interface.
---
## 4. Deep Copy Everywhere
**Source:** Generated code in `zz_generated.deepcopy.go` files throughout the tree
### What it does
Every API type has generated `DeepCopy()` and `DeepCopyInto()` methods that create true deep copies including nested slices, maps, and pointer fields.
### Why
The informer cache is shared across all controllers in a process. If controller A gets an object from the cache and mutates it, controller B would see corrupted data. Deep copy provides the isolation guarantee.
```go
// Usage pattern in controllers:
deployment := deploymentFromCache.DeepCopy()
deployment.Spec.Replicas = ptr.To[int32](3)
_, err := client.AppsV1().Deployments(ns).Update(ctx, deployment, metav1.UpdateOptions{})
```
### Key Insight
Stdlib rarely needs deep copy because stdlib objects are typically owned by one goroutine. Kubernetes has a **shared read cache** (the informer store) that necessitates copy-on-write semantics at the application level.
---
## 5. Graceful Shutdown with Priority Classes
**Source:** `pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go` (lines 23–100)
### What it does
When a node is shutting down, pods are terminated in priority order. Critical pods (system-node-critical) get more grace time than regular pods.
### Why
A hard kill of all pods simultaneously would lose important work. Priority-based graceful shutdown preserves the most important workloads longest.
```go
// pkg/kubelet/nodeshutdown/nodeshutdown_manager_linux.go:66-90
type managerImpl struct {
logger klog.Logger
recorder record.EventRecorder
getPods eviction.ActivePodsFunc
syncNodeStatus func(context.Context)
dbusCon dbusInhibiter
inhibitLock systemd.InhibitLock
nodeShuttingDownMutex sync.Mutex
nodeShuttingDownNow bool
podManager *podManager
}
```
---
## 6. Context as Logger Carrier
**Source:** `pkg/controller/deployment/deployment_controller.go` (lines 106, 179, 500)
### What it does
Kubernetes passes structured loggers through context:
```go
// pkg/controller/deployment/deployment_controller.go:179
logger := klog.FromContext(ctx)
logger.Info("Starting controller", "controller", "deployment")
```
### Why
At scale, you need structured logging with:
- Consistent key-value pairs (controller name, object reference)
- Verbosity levels (`logger.V(4).Info(...)`)
- No global state (context carries the logger configured by the caller)
### Key Insight
Stdlib's `log` package is global. Kubernetes uses context-based structured logging (`klog.FromContext`) to allow each call chain to carry its own logger configuration. This enables filtering by controller, verbosity tuning per-component, and correlation.
---
## 7. Functional Options for Configuration
**Source:** `staging/src/k8s.io/client-go/informers/factory.go` (lines 83–127)
### What it does
The SharedInformerFactory uses functional options for configuration:
```go
// staging/src/k8s.io/client-go/informers/factory.go:57
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
func WithTransform(transform cache.TransformFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.transform = transform
return factory
}
}
func NewSharedInformerFactoryWithOptions(client kubernetes.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{...}
for _, opt := range options {
factory = opt(factory)
}
return factory
}
```
### Why
APIs evolve. Adding a new configuration option shouldn't break callers. Functional options provide:
- Backward compatibility (new options don't change existing signatures)
- Self-documenting (each option is a named function)
- Composability (options can be collected and applied conditionally)
---
## 8. Type-Safe Generics in Critical Paths
**Source:** `staging/src/k8s.io/client-go/util/workqueue/queue.go` (lines 33–200), `staging/src/k8s.io/client-go/gentype/type.go` (lines 33–120)
### What it does
Both workqueue and gentype use Go generics (1.18+) to provide type-safe interfaces while maintaining backward compatibility via type aliases:
```go
// Workqueue: type-safe queue
type TypedInterface[T comparable] interface {
Add(item T)
Get() (item T, shutdown bool)
Done(item T)
}
// Type alias for backward compat
type Type = Typed[any]
// Gentype: type-safe client
type Client[T objectWithMeta] struct {
resource string
client rest.Interface
namespace string
newObject func() T
}
```
### Why
Before generics, Kubernetes used `interface{}` everywhere, requiring type assertions at every boundary. Generics eliminate entire classes of runtime panics and make the code self-documenting.
### Key Insight
This is a migration pattern: introduce the generic version alongside the deprecated `interface{}` version using type aliases. Callers migrate at their own pace.
---
## 9. HandleCrash — Structured Panic Recovery
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/runtime/runtime.go` (lines 30–120)
### What it does
A standardized `defer HandleCrash()` pattern that:
1. Catches panics
2. Logs them with proper stack attribution
3. Invokes registered panic handlers
4. Optionally re-panics (controlled by `ReallyCrash` flag)
```go
// staging/src/k8s.io/apimachinery/pkg/util/runtime/runtime.go:78-82
func HandleCrashWithContext(ctx context.Context, additionalHandlers ...func(context.Context, interface{})) {
if r := recover(); r != nil {
handleCrash(ctx, r, additionalHandlers...)
}
}
```
### Why
In a production system with hundreds of goroutines, an unrecovered panic in one kills the entire process. HandleCrash provides a standardized recovery point that:
- Logs the panic with caller attribution
- Allows cleanup handlers (shutdown gracefully)
- In tests, can be configured to not actually crash
### Key Insight
Stdlib's approach is "let it crash." Kubernetes' approach is "catch it, log it, let the controller retry on the next sync." This is only safe because the controller pattern is idempotent.
---
## 10. ContextForChannel — Bridge Pattern
**Source:** `staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go` (lines 120–145)
### What it does
Bridges the older `<-chan struct{}` stop pattern to the modern `context.Context` pattern:
```go
// staging/src/k8s.io/apimachinery/pkg/util/wait/wait.go:120-142
func ContextForChannel(parentCh <-chan struct{}) context.Context {
return channelContext{stopCh: parentCh}
}
type channelContext struct {
stopCh <-chan struct{}
}
func (c channelContext) Done() <-chan struct{} { return c.stopCh }
func (c channelContext) Err() error {
select {
case <-c.stopCh:
return context.Canceled
default:
return nil
}
}
```
### Why
Kubernetes predates `context.Context` (which arrived in Go 1.7). Millions of lines of code use `stopCh <-chan struct{}`. Rather than a big-bang rewrite, this adapter allows gradual migration.
### Key Insight
**Large codebases can't do breaking API changes atomically.** This bridge pattern is how you evolve from one idiom to another over years without breaking everything at once.
+205 -12
View File
@@ -1,10 +1,12 @@
# API Conventions in the Go Standard Library # API Conventions in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. The Must Pattern ## 1. The Must Pattern
**Pattern name:** MustXxx (Panic on Error) **Pattern name:** MustXxx (Panic on Error)
**Source citation:** `regexp/regexp.go` lines 310–320, `text/template/helper.go` lines 19–30 **Source citation:** [regexp/regexp.go#L310](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L310), [text/template/helper.go#L19](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/text/template/helper.go#L19)
**What it does:** A function wraps a fallible constructor and panics if the error **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). is non-nil. Named `MustXxx` or `Must` (when wrapping a generic `(T, error)` pair).
@@ -13,6 +15,67 @@ is non-nil. Named `MustXxx` or `Must` (when wrapping a generic `(T, error)` pair
`var` initializers can't handle errors, `Must` converts programmer errors (bad `var` initializers can't handle errors, `Must` converts programmer errors (bad
regex literals, bad templates) into immediate panics that surface during init. 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)
- `var` initialization can't handle the `(T, error)` return
**Example — before:**
```go
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:**
```go
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:**
```go
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:**
```go
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; **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`). panicking on recoverable errors; naming it something other than Must (e.g., `PanicOnError`).
@@ -53,7 +116,7 @@ func Must(t *Template, err error) *Template {
**Pattern name:** Fallible Constructor + Must Wrapper **Pattern name:** Fallible Constructor + Must Wrapper
**Source citation:** `regexp/regexp.go` lines 130–131, 310–320 **Source citation:** [regexp/regexp.go#L130](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L130), 310–320
**What it does:** The real constructor returns `(*T, error)`. A parallel `Must` variant **What it does:** The real constructor returns `(*T, error)`. A parallel `Must` variant
wraps it for use in global variable initialization. wraps it for use in global variable initialization.
@@ -88,7 +151,7 @@ func MustCompile(str string) *Regexp {
**Pattern name:** WithContext Function Overload **Pattern name:** WithContext Function Overload
**Source citation:** `net/http/request.go` lines 867–869, 894–930 **Source citation:** [net/http/request.go#L867](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/request.go#L867), 894–930
**What it does:** Provides two function variants: `NewRequest` (uses `context.Background()`) **What it does:** Provides two function variants: `NewRequest` (uses `context.Background()`)
and `NewRequestWithContext` (accepts an explicit context). The simple version delegates and `NewRequestWithContext` (accepts an explicit context). The simple version delegates
@@ -121,7 +184,7 @@ func NewRequestWithContext(ctx context.Context, method, url string, body io.Read
**Pattern name:** `*Options` Parameter — Nil Means Defaults **Pattern name:** `*Options` Parameter — Nil Means Defaults
**Source citation:** `log/slog/text_handler.go` lines 28–42, `log/slog/handler.go` lines 135–175 **Source citation:** [log/slog/text_handler.go#L28](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/text_handler.go#L28), [log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135)
**What it does:** A constructor accepts a pointer to an options struct. If the pointer **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. is nil, all defaults apply. The constructor internally substitutes a zero-value struct.
@@ -160,7 +223,7 @@ func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
**Pattern name:** Builder (Write Methods + String/Bytes Finalizer) **Pattern name:** Builder (Write Methods + String/Bytes Finalizer)
**Source citation:** `strings/builder.go` lines 14–113 **Source citation:** [strings/builder.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L14)
**What it does:** A zero-value struct accumulates data via Write/WriteByte/WriteString **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 methods, then produces a final result via String(). The builder is not reusable after
@@ -184,14 +247,14 @@ type Builder struct {
buf []byte buf []byte
} }
// strings/builder.go:92-96 // strings/builder.go:112-116
func (b *Builder) WriteString(s string) (int, error) { func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck() b.copyCheck()
b.buf = append(b.buf, s...) b.buf = append(b.buf, s...)
return len(s), nil return len(s), nil
} }
// strings/builder.go:48-50 // strings/builder.go:46-48
func (b *Builder) String() string { func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf)) return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
} }
@@ -203,7 +266,7 @@ func (b *Builder) String() string {
**Pattern name:** Convenience Wrappers over Configurable Core **Pattern name:** Convenience Wrappers over Configurable Core
**Source citation:** `os/file.go` lines 385–415 **Source citation:** [os/file.go#L385](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L385)
**What it does:** Simple functions (`Open`, `Create`) delegate to the fully configurable **What it does:** Simple functions (`Open`, `Create`) delegate to the fully configurable
`OpenFile` with pre-set flags. Users choose their level of control. `OpenFile` with pre-set flags. Users choose their level of control.
@@ -211,6 +274,59 @@ func (b *Builder) String() string {
**Why:** 90% of file opens are reads or creates. Layered APIs serve the common case **Why:** 90% of file opens are reads or creates. Layered APIs serve the common case
without hiding power. The naming makes intent clear. 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:**
```go
// User must know about flags for every file open
f, err := os.OpenFile("data.json", os.O_RDONLY, 0)
// Every. Single. Time.
```
**Example — after:**
```go
// 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., `Create` hides 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:**
```go
// 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:**
```go
// 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 **Anti-pattern:** Only exposing the full-power version; making users learn flag
constants for simple reads; duplicating implementation across convenience functions. constants for simple reads; duplicating implementation across convenience functions.
@@ -241,7 +357,7 @@ func OpenFile(name string, flag int, perm FileMode) (*File, error) {
**Pattern name:** Convenience Package Functions **Pattern name:** Convenience Package Functions
**Source citation:** `net/http/client.go` line 109, implied by `http.Get`, `http.Post` **Source citation:** [net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/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 **What it does:** Top-level functions like `http.Get(url)` call methods on the
`DefaultClient`. Users can bypass by creating their own `Client`. `DefaultClient`. Users can bypass by creating their own `Client`.
@@ -270,7 +386,7 @@ var DefaultClient = &Client{}
**Pattern name:** RegisterXxx for Side-Effect Imports **Pattern name:** RegisterXxx for Side-Effect Imports
**Source citation:** `crypto/crypto.go` lines 145–150 **Source citation:** [crypto/crypto.go#L145](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L145)
**What it does:** A `RegisterHash(h Hash, f func() hash.Hash)` function allows **What it does:** A `RegisterHash(h Hash, f func() hash.Hash)` function allows
algorithm implementations in sub-packages to register themselves via `init()`. algorithm implementations in sub-packages to register themselves via `init()`.
@@ -301,7 +417,7 @@ func RegisterHash(h Hash, f func() hash.Hash) {
**Pattern name:** Close vs Shutdown (Immediate vs Graceful) **Pattern name:** Close vs Shutdown (Immediate vs Graceful)
**Source citation:** `net/http/server.go` lines 3171–3220 (Close), 3221+ (Shutdown) **Source citation:** [net/http/server.go#L3171](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3171) (Close), 3221+ (Shutdown)
**What it does:** Provides both `Close()` (immediate, forceful) and `Shutdown(ctx)` **What it does:** Provides both `Close()` (immediate, forceful) and `Shutdown(ctx)`
(graceful, waits for in-flight requests). The context on Shutdown provides a (graceful, waits for in-flight requests). The context on Shutdown provides a
@@ -311,6 +427,81 @@ timeout mechanism.
Graceful shutdown is critical for production services; immediate close is needed for Graceful shutdown is critical for production services; immediate close is needed for
tests and emergency stops. 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:**
```go
type Server struct { listener net.Listener }
func (s *Server) Stop() {
s.listener.Close() // all in-flight requests get connection reset — data loss
}
```
**Example — after:**
```go
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.Exit` is your shutdown
**Over-application example:**
```go
// 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:**
```go
// 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 **Anti-pattern:** Only providing one shutdown mode; not accepting a context for
timeout control; leaking goroutines on shutdown. timeout control; leaking goroutines on shutdown.
@@ -341,7 +532,7 @@ func (s *Server) Shutdown(ctx context.Context) error {
**Pattern name:** NewXxx Returning Channel-Bearing Struct **Pattern name:** NewXxx Returning Channel-Bearing Struct
**Source citation:** `time/tick.go` lines 16–45, `time/sleep.go` lines 89–155 **Source citation:** [time/tick.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/tick.go#L16), [time/sleep.go#L89](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/sleep.go#L89)
**What it does:** `NewTicker(d)` and `NewTimer(d)` return structs with a `C <-chan Time` **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. field. Consumers select on the channel to receive time events.
@@ -372,3 +563,5 @@ func NewTicker(d Duration) *Ticker {
return t return t
} }
``` ```
<!-- PATTERN_COMPLETE -->
+385 -29
View File
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
## 1. sync.Mutex — The Basic Lock ## 1. sync.Mutex — The Basic Lock
### Source: `src/sync/mutex.go:18-34`, `src/sync/mutex.go:42-67` ### Source: [src/sync/mutex.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L18), [src/sync/mutex.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L42)
```go ```go
// src/sync/mutex.go:18-34 // [src/sync/mutex.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L18)
// A Mutex is a mutual exclusion lock. // A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex. // The zero value for a Mutex is an unlocked mutex.
// //
@@ -19,13 +19,13 @@ type Mutex struct {
mu isync.Mutex mu isync.Mutex
} }
// src/sync/mutex.go:36-39 // [src/sync/mutex.go#L36](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L36)
type Locker interface { type Locker interface {
Lock() Lock()
Unlock() Unlock()
} }
// src/sync/mutex.go:43-46 // [src/sync/mutex.go#L43](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L43)
func (m *Mutex) Lock() { func (m *Mutex) Lock() {
m.mu.Lock() m.mu.Lock()
} }
@@ -38,6 +38,85 @@ func (m *Mutex) Lock() {
- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock - **Not associated with a goroutine** — one goroutine can Lock, another can Unlock
- **Locker interface** — abstracts over Mutex and RWMutex - **Locker interface** — abstracts over Mutex and RWMutex
### When to Use
**Triggers:**
- Multiple goroutines read AND write the same data structure
- You need to protect a small critical section (a few field accesses)
- A channel-based solution would add complexity without benefit (no coordination needed, just protection)
**Example — before:**
```go
type Stats struct {
hits int
misses int
}
func (s *Stats) RecordHit() { s.hits++ } // DATA RACE when called from multiple goroutines
func (s *Stats) RecordMiss() { s.misses++ } // DATA RACE
```
**Example — after:**
```go
type Stats struct {
mu sync.Mutex
hits int
misses int
}
func (s *Stats) RecordHit() {
s.mu.Lock()
defer s.mu.Unlock()
s.hits++
}
```
### When NOT to Use
**Don't use this when:**
- Goroutines need to coordinate or communicate (not just protect state) — use channels
- The critical section involves blocking I/O (holding a mutex during network calls starves other goroutines)
- You can restructure to have a single goroutine own the state (no sharing = no lock needed)
**Over-application example:**
```go
// Using a mutex to coordinate work between goroutines
type TaskQueue struct {
mu sync.Mutex
tasks []Task
ready bool
}
func (q *TaskQueue) WaitForReady() {
for {
q.mu.Lock()
if q.ready {
q.mu.Unlock()
return
}
q.mu.Unlock()
time.Sleep(10 * time.Millisecond) // spin-waiting with a lock — terrible
}
}
```
**Better alternative:**
```go
// Use a channel for coordination/signaling
type TaskQueue struct {
tasks chan Task
ready chan struct{}
}
func (q *TaskQueue) WaitForReady() {
<-q.ready // blocks efficiently, no spinning
}
```
**Why:** Mutexes protect data; channels coordinate goroutines. If you're polling a mutex-protected flag, you've reinvented a bad channel. The Go proverb applies: "Don't communicate by sharing memory; share memory by communicating."
### Idiomatic Usage ### Idiomatic Usage
```go ```go
@@ -67,24 +146,24 @@ mu.Unlock()
## 2. sync.Once — Exactly-Once Initialization ## 2. sync.Once — Exactly-Once Initialization
### Source: `src/sync/once.go:12-36`, `src/sync/once.go:56-79` ### Source: [src/sync/once.go#L12](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L12), [src/sync/once.go#L56](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L56)
```go ```go
// src/sync/once.go:12-23 // [src/sync/once.go#L12](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L12)
type Once struct { type Once struct {
_ noCopy _ noCopy
done atomic.Bool done atomic.Bool
m Mutex m Mutex
} }
// src/sync/once.go:56-63 // [src/sync/once.go#L56](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L56)
func (o *Once) Do(f func()) { func (o *Once) Do(f func()) {
if !o.done.Load() { if !o.done.Load() {
o.doSlow(f) o.doSlow(f)
} }
} }
// src/sync/once.go:65-72 // [src/sync/once.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L65)
func (o *Once) doSlow(f func()) { func (o *Once) doSlow(f func()) {
o.m.Lock() o.m.Lock()
defer o.m.Unlock() defer o.m.Unlock()
@@ -101,6 +180,97 @@ The implementation reveals a subtle guarantee: **when Do returns, f has finished
The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27). The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27).
### When to Use
**Triggers:**
- You have expensive initialization that should happen exactly once (DB connection, config parse, compiled regex)
- Multiple goroutines may trigger the initialization concurrently
- You're using `var` + `if instance == nil` checks that aren't goroutine-safe
**Example — before:**
```go
var db *sql.DB
func GetDB() *sql.DB {
if db == nil { // RACE: two goroutines can both see nil
db, _ = sql.Open("postgres", connStr)
}
return db
}
```
**Example — after:**
```go
var (
db *sql.DB
once sync.Once
)
func GetDB() *sql.DB {
once.Do(func() {
db, _ = sql.Open("postgres", connStr)
})
return db
}
```
### When NOT to Use
**Don't use this when:**
- Initialization can fail and you need to retry — Once runs the func at most once, even on failure
- You need to reset/reinitialize later (Once has no Reset method)
- The initialization is cheap — just do it in `init()` or at declaration time
**Over-application example:**
```go
var (
conn *grpc.ClientConn
once sync.Once
)
func GetConn() (*grpc.ClientConn, error) {
var err error
once.Do(func() {
conn, err = grpc.Dial("server:443")
})
if err != nil {
return nil, err // PROBLEM: next call to GetConn returns nil conn, nil err
// because once.Do won't run again
}
return conn, nil
}
```
**Better alternative:**
```go
// Use sync.OnceValues (Go 1.21+) which caches both value and error,
// or handle retry logic explicitly
var getConn = sync.OnceValues(func() (*grpc.ClientConn, error) {
return grpc.Dial("server:443")
})
// Or for retry scenarios, use a mutex with a nil check
var (
mu sync.Mutex
conn *grpc.ClientConn
)
func GetConn() (*grpc.ClientConn, error) {
mu.Lock()
defer mu.Unlock()
if conn != nil {
return conn, nil
}
var err error
conn, err = grpc.Dial("server:443") // retries on next call if this fails
return conn, err
}
```
**Why:** `sync.Once` guarantees exactly-once execution regardless of success or failure. If the initialization can fail transiently (network timeout, service unavailable), Once will permanently cache the failure. Use `sync.OnceValues` for caching results, or a mutex with a nil-check pattern when retry is needed.
### Idiomatic Usage ### Idiomatic Usage
```go ```go
@@ -131,10 +301,10 @@ once.Do(func() {
## 3. sync.WaitGroup — Waiting for Goroutine Completion ## 3. sync.WaitGroup — Waiting for Goroutine Completion
### Source: `src/sync/waitgroup.go:14-43`, `src/sync/waitgroup.go:236-260` ### Source: [src/sync/waitgroup.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L14), [src/sync/waitgroup.go#L236](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L236)
```go ```go
// src/sync/waitgroup.go:14-43 // [src/sync/waitgroup.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L14)
// Typically, a main goroutine will start tasks by calling WaitGroup.Go // Typically, a main goroutine will start tasks by calling WaitGroup.Go
// and then wait for all tasks to complete by calling WaitGroup.Wait: // and then wait for all tasks to complete by calling WaitGroup.Wait:
// //
@@ -152,7 +322,7 @@ type WaitGroup struct {
### Go 1.25+: WaitGroup.Go ### Go 1.25+: WaitGroup.Go
```go ```go
// src/sync/waitgroup.go:236-260 // [src/sync/waitgroup.go#L236](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L236)
func (wg *WaitGroup) Go(f func()) { func (wg *WaitGroup) Go(f func()) {
wg.Add(1) wg.Add(1)
go func() { go func() {
@@ -204,10 +374,10 @@ wg.Wait()
## 4. sync.Pool — Object Reuse for GC Pressure ## 4. sync.Pool — Object Reuse for GC Pressure
### Source: `src/sync/pool.go:44-63` ### Source: [src/sync/pool.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
```go ```go
// src/sync/pool.go:44-63 // [src/sync/pool.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
// Pool's purpose is to cache allocated but unused items for later reuse, // Pool's purpose is to cache allocated but unused items for later reuse,
// relieving pressure on the garbage collector. That is, it makes it easy to // relieving pressure on the garbage collector. That is, it makes it easy to
// build efficient, thread-safe free lists. // build efficient, thread-safe free lists.
@@ -269,15 +439,15 @@ pool.Put(buf) // still has data from last use
## 5. Channel as Done Signal (Context Pattern) ## 5. Channel as Done Signal (Context Pattern)
### Source: `src/context/context.go:83-100` (Done channel), `src/io/pipe.go:42-45` ### Source: [src/context/context.go#L83](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L83) (Done channel), [src/io/pipe.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L42)
```go ```go
// src/context/context.go:83-100 // [src/context/context.go#L83](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L83)
// Done returns a channel that's closed when work done on behalf of this // Done returns a channel that's closed when work done on behalf of this
// context should be canceled. // context should be canceled.
Done() <-chan struct{} Done() <-chan struct{}
// src/io/pipe.go:42-45 // [src/io/pipe.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L42)
type pipe struct { type pipe struct {
once sync.Once once sync.Once
done chan struct{} // closed on pipe close done chan struct{} // closed on pipe close
@@ -297,6 +467,105 @@ case result := <-work:
} }
``` ```
### When to Use
**Triggers:**
- You need to broadcast "stop" to multiple goroutines simultaneously
- A goroutine needs to select between work and cancellation
- You're implementing graceful shutdown for a long-running service
**Example — before:**
```go
type Server struct {
stopped bool // RACE: no synchronization
}
func (s *Server) worker() {
for {
if s.stopped { return } // busy-polls, racy
doWork()
}
}
```
**Example — after:**
```go
type Server struct {
done chan struct{}
}
func NewServer() *Server {
return &Server{done: make(chan struct{})}
}
func (s *Server) worker() {
for {
select {
case <-s.done:
return
case work := <-s.workCh:
process(work)
}
}
}
func (s *Server) Stop() { close(s.done) } // broadcasts to ALL workers
```
### When NOT to Use
**Don't use this when:**
- You need to send actual data between goroutines — use a typed channel
- You only have one goroutine to signal — a simple `return` or function call suffices
- You're using it as a poor substitute for `context.Context` (which already provides Done())
**Over-application example:**
```go
// Rolling your own done channel when context already provides this
type Worker struct {
done chan struct{}
ctx context.Context
cancel context.CancelFunc
}
func (w *Worker) Start() {
go func() {
select {
case <-w.done: // redundant — ctx.Done() does the same thing
return
case <-w.ctx.Done():
return
}
}()
}
```
**Better alternative:**
```go
// Just use the context — it already IS a done signal
type Worker struct {
ctx context.Context
cancel context.CancelFunc
}
func (w *Worker) Start() {
go func() {
select {
case <-w.ctx.Done():
return
case work := <-w.workCh:
process(work)
}
}()
}
func (w *Worker) Stop() { w.cancel() }
```
**Why:** If you already have a `context.Context`, its `Done()` channel is your cancellation signal. Adding a separate `chan struct{}` duplicates functionality and creates two shutdown paths that must be kept in sync. Use raw done channels only when you don't have a context (e.g., standalone libraries that predate context).
### Anti-pattern ### Anti-pattern
```go ```go
@@ -314,10 +583,10 @@ close(done)
## 6. Context Propagation Rules ## 6. Context Propagation Rules
### Source: `src/context/context.go:37-48` ### Source: [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
```go ```go
// src/context/context.go:37-48 // [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
// Do not store Contexts inside a struct type; instead, pass a Context // Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first // explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx: // parameter, typically named ctx:
@@ -353,10 +622,10 @@ func doWork(data Data, ctx context.Context) // wrong position
## 7. Context Cancellation with Timeout ## 7. Context Cancellation with Timeout
### Source: `src/net/http/server.go:4007-4050` (TimeoutHandler) ### Source: [src/net/http/server.go#L4007](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4007) (TimeoutHandler)
```go ```go
// src/net/http/server.go:4011-4050 // [src/net/http/server.go#L4011](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4011)
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) { func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt) ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
defer cancelCtx() defer cancelCtx()
@@ -406,10 +675,10 @@ func longWork(ctx context.Context) {
## 8. Select with Non-Blocking Check ## 8. Select with Non-Blocking Check
### Source: `src/io/pipe.go:51-60` ### Source: [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
```go ```go
// src/io/pipe.go:51-60 // [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
func (p *pipe) read(b []byte) (n int, err error) { func (p *pipe) read(b []byte) (n int, err error) {
select { select {
case <-p.done: case <-p.done:
@@ -450,17 +719,17 @@ for {
## 9. Channel Pipeline (io.Pipe) ## 9. Channel Pipeline (io.Pipe)
### Source: `src/io/pipe.go:38-45`, `src/io/pipe.go:195-205` ### Source: [src/io/pipe.go#L38](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L38), [src/io/pipe.go#L195](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L195)
```go ```go
// src/io/pipe.go:38-45 // [src/io/pipe.go#L38](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L38)
type pipe struct { type pipe struct {
wrCh chan []byte // writer sends data slices wrCh chan []byte // writer sends data slices
rdCh chan int // reader returns bytes consumed rdCh chan int // reader returns bytes consumed
done chan struct{} done chan struct{}
} }
// src/io/pipe.go:195-205 // [src/io/pipe.go#L195](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L195)
func Pipe() (*PipeReader, *PipeWriter) { func Pipe() (*PipeReader, *PipeWriter) {
pw := &PipeWriter{r: PipeReader{pipe: pipe{ pw := &PipeWriter{r: PipeReader{pipe: pipe{
wrCh: make(chan []byte), // unbuffered wrCh: make(chan []byte), // unbuffered
@@ -494,6 +763,91 @@ func generate(ctx context.Context) <-chan int {
} }
``` ```
### When to Use
**Triggers:**
- You have a producer-consumer flow where the consumer's speed should limit the producer (backpressure)
- Data flows through multiple transformation stages
- You want to decouple stages that can run concurrently
**Example — before:**
```go
func processAll(items []string) []Result {
var results []Result
for _, item := range items {
fetched := fetch(item) // sequential: fetch then transform
results = append(results, transform(fetched))
}
return results
}
```
**Example — after:**
```go
func processAll(ctx context.Context, items []string) []Result {
fetched := make(chan Fetched)
go func() {
defer close(fetched)
for _, item := range items {
select {
case fetched <- fetch(item): // backpressure: blocks if transform is slow
case <-ctx.Done():
return
}
}
}()
var results []Result
for f := range fetched {
results = append(results, transform(f))
}
return results
}
```
### When NOT to Use
**Don't use this when:**
- The "pipeline" has only one stage — you're just adding goroutine overhead to sequential code
- All items are already in memory and processing is CPU-bound with no I/O — `for` loop is simpler and faster
- You don't actually need backpressure (data fits in memory, producer is finite)
**Over-application example:**
```go
// Channel pipeline for a simple in-memory transformation
func doubleAll(nums []int) []int {
ch := make(chan int)
go func() {
defer close(ch)
for _, n := range nums {
ch <- n // channel overhead for no benefit
}
}()
var result []int
for n := range ch {
result = append(result, n*2)
}
return result
}
```
**Better alternative:**
```go
// Just use a loop — no concurrency needed
func doubleAll(nums []int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = n * 2
}
return result
}
```
**Why:** Channel pipelines shine when stages involve I/O (network, disk) and can overlap waiting. For pure computation on in-memory data, the channel send/receive overhead (~50-100ns per item) adds up and the goroutine scheduling has no useful work to overlap with. A plain loop is faster, simpler, and easier to debug.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -514,10 +868,10 @@ func produce() <-chan int {
## 10. Background Worker with Context Shutdown ## 10. Background Worker with Context Shutdown
### Source: `src/database/sql/sql.go:836-843` ### Source: [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
```go ```go
// src/database/sql/sql.go:836-843 // [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
func OpenDB(c driver.Connector) *DB { func OpenDB(c driver.Connector) *DB {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
db := &DB{ db := &DB{
@@ -549,10 +903,10 @@ go func() {
## 11. noCopy — Preventing Value Copies ## 11. noCopy — Preventing Value Copies
### Source: `src/sync/cond.go:120-126` ### Source: [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
```go ```go
// src/sync/cond.go:120-126 // [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
type noCopy struct{} type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`. // Lock is a no-op used by -copylocks checker from `go vet`.
@@ -593,3 +947,5 @@ func doWork(wg *sync.WaitGroup) {
| Backpressure between producer/consumer | Unbuffered channels | | Backpressure between producer/consumer | Unbuffered channels |
| Long-lived background worker | Goroutine + context cancellation | | Long-lived background worker | Goroutine + context cancellation |
| Prevent struct copying | Embed `noCopy` field | | Prevent struct copying | Embed `noCopy` field |
<!-- PATTERN_COMPLETE -->
+989
View File
@@ -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#L575](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L575)
```go
// src/crypto/tls/common.go:575
// 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#L996](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L996) (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 -->
+132 -14
View File
@@ -1,10 +1,12 @@
# Documentation Patterns in the Go Standard Library # Documentation Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Package Documentation (doc.go or Package Comment) ## 1. Package Documentation (doc.go or Package Comment)
**Pattern name:** Package Doc Comment **Pattern name:** Package Doc Comment
**Source citation:** `net/http/doc.go` lines 6–30, `os/file.go` lines 5–43, `log/slog/doc.go` lines 6–30 **Source citation:** [net/http/doc.go#L6](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/doc.go#L6), [os/file.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L5), [log/slog/doc.go#L6](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/doc.go#L6)
**What it does:** The first file in a package (by convention `doc.go`, or the main **What it does:** The first file in a package (by convention `doc.go`, or the main
source file) starts with a `// Package xxx ...` comment that explains the package's source file) starts with a `// Package xxx ...` comment that explains the package's
@@ -57,7 +59,7 @@ expressed as key-value pairs.
**Pattern name:** `# Heading` in Doc Comments **Pattern name:** `# Heading` in Doc Comments
**Source citation:** `os/file.go` lines 37–43, `net/http/doc.go` (multiple sections) **Source citation:** [os/file.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L37), `net/http/doc.go` (multiple sections)
**What it does:** Uses `# Section Name` within the package doc comment to organize **What it does:** Uses `# Section Name` within the package doc comment to organize
long documentation into navigable sections. long documentation into navigable sections.
@@ -84,7 +86,7 @@ too many sections (fragmenting simple docs).
**Pattern name:** `// TypeName verb...` or `// FuncName verb...` **Pattern name:** `// TypeName verb...` or `// FuncName verb...`
**Source citation:** `net/http/server.go` lines 64–82 (Handler), `bufio/scan.go` lines 14–27 (Scanner) **Source citation:** [net/http/server.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L65) (Handler), [bufio/scan.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L14) (Scanner)
**What it does:** Every exported identifier's doc comment starts with the identifier **What it does:** Every exported identifier's doc comment starts with the identifier
name, followed by a verb phrase describing what it does or represents. name, followed by a verb phrase describing what it does or represents.
@@ -100,7 +102,7 @@ the comment entirely.
**Code examples from source:** **Code examples from source:**
```go ```go
// net/http/server.go:64 // net/http/server.go:65
// A Handler responds to an HTTP request. // A Handler responds to an HTTP request.
// bufio/scan.go:14-17 // bufio/scan.go:14-17
@@ -125,7 +127,7 @@ the comment entirely.
**Pattern name:** `[TypeName]`, `[Package.Symbol]`, `[Method]` Links **Pattern name:** `[TypeName]`, `[Package.Symbol]`, `[Method]` Links
**Source citation:** `net/http/server.go` lines 65–70, `os/file.go` line 9 **Source citation:** [net/http/server.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L65), [os/file.go#L9](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L9)
**What it does:** Doc comments use `[SymbolName]` to create hyperlinks to other **What it does:** Doc comments use `[SymbolName]` to create hyperlinks to other
identifiers. These render as clickable links on pkg.go.dev. identifiers. These render as clickable links on pkg.go.dev.
@@ -158,7 +160,7 @@ over-linking (every mention of every type).
**Pattern name:** `func ExampleXxx()` / `func ExampleType_Method()` **Pattern name:** `func ExampleXxx()` / `func ExampleType_Method()`
**Source citation:** `regexp/example_test.go` lines 13–28, `net/http/example_handle_test.go` lines 16–31 **Source citation:** [regexp/example_test.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/example_test.go#L13), [net/http/example_handle_test.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L16)
**What it does:** Functions named `Example`, `ExampleXxx`, or `ExampleType_Method` **What it does:** Functions named `Example`, `ExampleXxx`, or `ExampleType_Method`
in `_test.go` files serve as both executable tests and documentation. They include in `_test.go` files serve as both executable tests and documentation. They include
@@ -168,6 +170,66 @@ an `// Output:` comment that `go test` verifies.
in `go doc` and pkg.go.dev alongside the relevant symbol. They teach by showing in `go doc` and pkg.go.dev alongside the relevant symbol. They teach by showing
real, working code. real, working code.
**When to Use**
**Triggers:**
- You have a public function/type whose usage isn't obvious from the signature alone
- Your README examples have drifted from the actual API (broken examples in docs)
- You want examples that appear on pkg.go.dev AND are verified by `go test`
**Example — before:**
```go
// README.md (may be stale):
// ```go
// result := mylib.Process("input")
// fmt.Println(result.Data)
// ```
// ← compiles? who knows. API changed last month.
```
**Example — after:**
```go
// example_test.go
func ExampleProcess() {
result := mylib.Process("input")
fmt.Println(result.Data)
// Output:
// processed: input
}
// ← go test verifies this compiles and produces the expected output
```
**When NOT to Use**
**Don't write Example tests when:**
- The function signature is self-explanatory (`func Max(a, b int) int` doesn't need an example)
- The example would just restate the doc comment with no additional insight
- You're testing internal/unexported functions (examples must use the public API)
- The output is non-deterministic (timestamps, random values, goroutine ordering)
**Over-application example:**
```go
// Pointless — the signature tells you everything
func ExampleAbs() {
fmt.Println(math.Abs(-5))
// Output:
// 5
}
```
**Better alternative:**
```go
// Skip the example for trivial functions. Write examples for non-obvious behavior:
func ExampleAbs_nan() {
fmt.Println(math.Abs(math.NaN()))
// Output:
// NaN
}
// ← This teaches something surprising that the signature doesn't convey
```
**Why:** Examples exist to teach usage patterns that aren't obvious from the type signature and doc comment. Trivial examples add maintenance burden without teaching anything.
**Anti-pattern:** Examples that don't compile; examples without Output comments **Anti-pattern:** Examples that don't compile; examples without Output comments
(not verified); examples in README that drift from reality. (not verified); examples in README that drift from reality.
@@ -218,7 +280,7 @@ func ExampleHandle() {
**Pattern name:** Indented Code Blocks in Comments **Pattern name:** Indented Code Blocks in Comments
**Source citation:** `os/file.go` lines 17–35, `time/time.go` lines 928–933 **Source citation:** [os/file.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L16), [time/time.go#L928](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L928)
**What it does:** Doc comments include indented code snippets (4 spaces) that render **What it does:** Doc comments include indented code snippets (4 spaces) that render
as preformatted code blocks in godoc. as preformatted code blocks in godoc.
@@ -232,7 +294,7 @@ for inline (use Example functions instead); examples that reference unexported s
**Code examples from source:** **Code examples from source:**
```go ```go
// os/file.go:17-21 // os/file.go:16-21
// Here is a simple example, opening a file and reading some of it. // Here is a simple example, opening a file and reading some of it.
// //
// file, err := os.Open("file.go") // For read access. // file, err := os.Open("file.go") // For read access.
@@ -240,7 +302,7 @@ for inline (use Example functions instead); examples that reference unexported s
// log.Fatal(err) // log.Fatal(err)
// } // }
// time/time.go:928-933 // time/time.go:925-933
// To count the number of units in a [Duration], divide: // To count the number of units in a [Duration], divide:
// //
// second := time.Second // second := time.Second
@@ -258,7 +320,7 @@ for inline (use Example functions instead); examples that reference unexported s
**Pattern name:** `// Deprecated: ...` in Doc Comments **Pattern name:** `// Deprecated: ...` in Doc Comments
**Source citation:** `net/http/server.go` line 57 (ErrWriteAfterFlush), `os/file.go` lines 93–95 **Source citation:** [net/http/server.go#L57](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L57) (ErrWriteAfterFlush), [os/file.go#L93](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L93)
**What it does:** A paragraph starting with `Deprecated:` marks an identifier as **What it does:** A paragraph starting with `Deprecated:` marks an identifier as
deprecated and explains what to use instead. deprecated and explains what to use instead.
@@ -266,13 +328,67 @@ deprecated and explains what to use instead.
**Why:** Recognized by tooling (go vet, staticcheck, IDEs). Provides a migration **Why:** Recognized by tooling (go vet, staticcheck, IDEs). Provides a migration
path without breaking backward compatibility. path without breaking backward compatibility.
**When to Use**
**Triggers:**
- You have a better replacement for an existing function but can't remove the old one (semver)
- Users are still calling a function that has known issues or a superior alternative
- You want IDEs to show a strikethrough and linters to warn on usage
**Example — before:**
```go
// Just delete it? Breaks everyone's code.
// Leave it silently? Users never learn about the better way.
```
**Example — after:**
```go
// ParseDuration parses a duration string.
//
// Deprecated: Use [time.ParseDuration] instead, which handles
// all standard duration formats.
func ParseDuration(s string) (time.Duration, error) {
return time.ParseDuration(s) // delegate to the replacement
}
```
**When NOT to Use**
**Don't deprecate when:**
- The function still works fine and there's no better replacement yet
- You're deprecating to "clean up" the API without a migration path (users will just ignore it)
- The replacement has a different behavior or isn't a drop-in substitute (document the difference instead)
- You're on v0.x and can just remove it (pre-1.0 allows breaking changes)
**Over-application example:**
```go
// Deprecated: Use NewClientV2 instead.
func NewClient(addr string) *Client { ... }
// But NewClientV2 has a completely different API:
func NewClientV2(cfg Config) (*ClientV2, error) { ... }
// Users can't just find-and-replace — this isn't a deprecation, it's a migration
```
**Better alternative:**
```go
// NewClient creates a client with default configuration.
// For advanced configuration (TLS, timeouts, connection pooling),
// use [NewClientWithConfig] instead.
func NewClient(addr string) *Client {
return NewClientWithConfig(Config{Addr: addr})
}
```
**Why:** Deprecation means "there's a better way to do the same thing." If the replacement requires a fundamentally different approach, provide a migration guide — don't just slap `Deprecated:` on it and leave users stranded.
**Anti-pattern:** Removing deprecated APIs (breaks semver); deprecating without **Anti-pattern:** Removing deprecated APIs (breaks semver); deprecating without
suggesting an alternative; using non-standard deprecation markers. suggesting an alternative; using non-standard deprecation markers.
**Code example from source:** **Code example from source:**
```go ```go
// net/http/server.go:55-57 // net/http/server.go:59-62
// Deprecated: ErrWriteAfterFlush is no longer returned by // Deprecated: ErrWriteAfterFlush is no longer returned by
// anything in the net/http package. Callers should not // anything in the net/http package. Callers should not
// compare errors against this variable. // compare errors against this variable.
@@ -285,7 +401,7 @@ ErrWriteAfterFlush = errors.New("unused")
**Pattern name:** "If there is an error, it will be of type [*XxxError]" **Pattern name:** "If there is an error, it will be of type [*XxxError]"
**Source citation:** `os/file.go` lines 388, 406 **Source citation:** [os/file.go#L388](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L388), 406
**What it does:** Functions document the concrete error type they return, enabling **What it does:** Functions document the concrete error type they return, enabling
callers to type-assert for additional context. callers to type-assert for additional context.
@@ -314,7 +430,7 @@ func Open(name string) (*File, error) {
**Pattern name:** "Safe for concurrent use" / Concurrency Guarantees **Pattern name:** "Safe for concurrent use" / Concurrency Guarantees
**Source citation:** `net/http/transport.go` lines 79–80, `os/types.go` line 17, `regexp/regexp.go` lines 77–79 **Source citation:** [net/http/transport.go#L79](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L79), [os/types.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/types.go#L17), [regexp/regexp.go#L77](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L77)
**What it does:** Doc comments explicitly state the concurrency safety of a type **What it does:** Doc comments explicitly state the concurrency safety of a type
or note exceptions where concurrent use is not safe. or note exceptions where concurrent use is not safe.
@@ -329,7 +445,7 @@ concurrent use by multiple goroutines").
**Code examples from source:** **Code examples from source:**
```go ```go
// net/http/transport.go:79-80 // net/http/transport.go:72-73
// Transports should be reused instead of created as needed. // Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines. // Transports are safe for concurrent use by multiple goroutines.
@@ -340,3 +456,5 @@ concurrent use by multiple goroutines").
// A Regexp is safe for concurrent use by multiple goroutines, // A Regexp is safe for concurrent use by multiple goroutines,
// except for configuration methods, such as [Regexp.Longest]. // except for configuration methods, such as [Regexp.Longest].
``` ```
<!-- PATTERN_COMPLETE -->
+252 -25
View File
@@ -6,21 +6,21 @@ Patterns extracted from the Go standard library source code.
## 1. Sentinel Errors ## 1. Sentinel Errors
### Source: `src/io/io.go:40-43` (EOF), `src/errors/errors.go:81-83` (ErrUnsupported) ### Source: [src/io/io.go#L40](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L40) (EOF), [src/errors/errors.go#L81](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L81) (ErrUnsupported)
```go ```go
// src/io/io.go:40-43 // [src/io/io.go#L40](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L40)
// EOF is the error returned by Read when no more input is available. // EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF, // (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.) // because callers will test for EOF using ==.)
var EOF = errors.New("EOF") var EOF = errors.New("EOF")
// src/io/io.go:47-49 // [src/io/io.go#L47](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L47)
var ErrUnexpectedEOF = errors.New("unexpected EOF") var ErrUnexpectedEOF = errors.New("unexpected EOF")
``` ```
```go ```go
// src/errors/errors.go:81-83 // [src/errors/errors.go#L81](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L81)
var ErrUnsupported = New("unsupported operation") var ErrUnsupported = New("unsupported operation")
``` ```
@@ -36,6 +36,86 @@ if err == io.EOF {
**Critical rule from io.EOF's doc comment**: Read must return EOF itself, **not an error wrapping EOF**, because callers test for it with `==`. This is the distinction between sentinel errors (identity-checked) and wrapped errors (tree-checked). **Critical rule from io.EOF's doc comment**: Read must return EOF itself, **not an error wrapping EOF**, because callers test for it with `==`. This is the distinction between sentinel errors (identity-checked) and wrapped errors (tree-checked).
### When to Use
**Triggers:**
- You have a specific, well-known failure condition callers need to check by identity
- Multiple packages compare against the same error value (`io.EOF`, `sql.ErrNoRows`)
- The error represents a **state** ("end of stream", "not found"), not a bug
**Example — before:**
```go
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ...")
var u User
err := row.Scan(&u.Name)
if err != nil {
return nil, fmt.Errorf("user not found") // caller can't distinguish "not found" from "db down"
}
return &u, nil
}
```
**Example — after:**
```go
var ErrUserNotFound = errors.New("users: not found")
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ...")
var u User
err := row.Scan(&u.Name)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound // sentinel: callers can test with errors.Is
}
if err != nil {
return nil, fmt.Errorf("fetchUser: %w", err)
}
return &u, nil
}
```
### When NOT to Use
**Don't use this when:**
- The error condition is internal and no caller should branch on it — use `fmt.Errorf` instead
- You have too many sentinels (>5-6 per package) — consider a custom error type with a code field
- The error carries varying context (file path, user ID) — sentinels are for fixed conditions
**Over-application example:**
```go
// A sentinel for every possible failure — explosion of package-level vars
var (
ErrUserNotFound = errors.New("users: not found")
ErrUserInactive = errors.New("users: inactive")
ErrUserSuspended = errors.New("users: suspended")
ErrUserRateLimited = errors.New("users: rate limited")
ErrUserInvalidEmail = errors.New("users: invalid email")
ErrUserInvalidName = errors.New("users: invalid name")
ErrUserInvalidAge = errors.New("users: invalid age")
// 20 more...
)
```
**Better alternative:**
```go
// Use a typed error with a code when you have many distinct conditions
type UserError struct {
Code string // "not_found", "inactive", "suspended"
Field string // which field failed validation
Message string
}
func (e *UserError) Error() string { return "users: " + e.Message }
// Callers use errors.As to inspect
var uerr *UserError
if errors.As(err, &uerr) && uerr.Code == "not_found" { ... }
```
**Why:** Sentinels are for a small number of well-known states that callers frequently branch on. If you're creating dozens, you've outgrown the pattern — a structured error type with an enum/code field scales better and avoids polluting the package namespace.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -52,15 +132,15 @@ func Read() error {
## 2. errors.New — Minimal Error Construction ## 2. errors.New — Minimal Error Construction
### Source: `src/errors/errors.go:62-69` ### Source: [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
```go ```go
// src/errors/errors.go:62-64 // [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
func New(text string) error { func New(text string) error {
return &errorString{text} return &errorString{text}
} }
// src/errors/errors.go:66-69 // [src/errors/errors.go#L66](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L66)
type errorString struct { type errorString struct {
s string s string
} }
@@ -97,10 +177,10 @@ func doThing() error {
## 3. Error Wrapping with fmt.Errorf and %w ## 3. Error Wrapping with fmt.Errorf and %w
### Source: `src/fmt/errors.go:13-23`, `src/fmt/errors.go:70-80` ### Source: [src/fmt/errors.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L13), [src/fmt/errors.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L70)
```go ```go
// src/fmt/errors.go:13-23 // [src/fmt/errors.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L13)
// Errorf formats according to a format specifier and returns the string // Errorf formats according to a format specifier and returns the string
// as a value that satisfies error. // as a value that satisfies error.
// //
@@ -110,7 +190,7 @@ func doThing() error {
// Unwrap method returning a []error containing all the %w operands. // Unwrap method returning a []error containing all the %w operands.
func Errorf(format string, a ...any) (err error) { ... } func Errorf(format string, a ...any) (err error) { ... }
// src/fmt/errors.go:70-80 // [src/fmt/errors.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L70)
type wrapError struct { type wrapError struct {
msg string msg string
err error err error
@@ -137,11 +217,85 @@ return fmt.Errorf("open config: %w", err)
return fmt.Errorf("open config: %v", err) return fmt.Errorf("open config: %v", err)
``` ```
### When to Use
**Triggers:**
- You're adding context to an error before returning it up the call stack
- The caller's error message would be meaningless without knowing *what* operation failed
- You have a chain of function calls and want a readable error trail: `"open config: read file: permission denied"`
**Example — before:**
```go
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // caller sees "open /etc/app.conf: permission denied" — no context about WHO called ReadFile
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err // caller can't tell if this was a read error or a parse error
}
return &cfg, nil
}
```
**Example — after:**
```go
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config: %w", err) // wraps: callers can errors.Is(err, os.ErrNotExist)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("load config: parse %s: %v", path, err) // %v: hides internal JSON error type
}
return &cfg, nil
}
```
### When to use %w vs %v ### When to use %w vs %v
- **%w**: When the wrapped error is part of your API contract. Callers can depend on it. - **%w**: When the wrapped error is part of your API contract. Callers can depend on it.
- **%v**: When you want to include the error text but NOT let callers depend on the underlying type. Use for implementation details. - **%v**: When you want to include the error text but NOT let callers depend on the underlying type. Use for implementation details.
### When NOT to Use
**Don't use this when:**
- You're wrapping at every single layer — the error message becomes `"a: b: c: d: e: permission denied"`
- The added context is obvious from the function name (the caller already knows what function they called)
- You're wrapping errors from a dependency you don't want in your API contract (use `%v` instead)
**Over-application example:**
```go
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
user, err := s.getUser(r)
if err != nil {
// "handleRequest: getUser: fetchFromDB: queryRow: scan: sql: no rows"
// The handler name adds nothing — the caller IS the handler
http.Error(w, fmt.Errorf("handleRequest: %w", err).Error(), 500)
return
}
}
```
**Better alternative:**
```go
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
user, err := s.getUser(r)
if err != nil {
// Log the full chain internally, return a clean message to the client
slog.Error("fetching user", "err", err, "path", r.URL.Path)
http.Error(w, "internal error", 500)
return
}
}
```
**Why:** Wrapping should add *meaningful* context that isn't already obvious. At HTTP handler boundaries, you typically log the error (with full chain) and return a generic response. Wrapping at every layer creates unreadable error strings and leaks internals.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -156,10 +310,10 @@ return fmt.Errorf("internal: %w", internalErr) // now callers depend on interna
## 4. errors.Is — Checking Error Identity Through Chains ## 4. errors.Is — Checking Error Identity Through Chains
### Source: `src/errors/wrap.go:30-44` ### Source: [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
```go ```go
// src/errors/wrap.go:30-44 // [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
func Is(err, target error) bool { func Is(err, target error) bool {
if err == nil || target == nil { if err == nil || target == nil {
return err == target return err == target
@@ -223,10 +377,10 @@ if errors.Is(err, os.ErrNotExist) { ... } // works through wrapping
## 5. errors.As — Extracting Error Types Through Chains ## 5. errors.As — Extracting Error Types Through Chains
### Source: `src/errors/wrap.go:96-120` ### Source: [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
```go ```go
// src/errors/wrap.go:96-120 // [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
func As(err error, target any) bool { func As(err error, target any) bool {
if err == nil { if err == nil {
return false return false
@@ -252,7 +406,7 @@ if errors.As(err, &pathErr) {
### Go 1.24+: errors.AsType (generic version) ### Go 1.24+: errors.AsType (generic version)
From `src/errors/errors.go:48-56` doc: From [src/errors/errors.go#L48](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L48) doc:
```go ```go
if perr, ok := errors.AsType[*fs.PathError](err); ok { if perr, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println(perr.Path) fmt.Println(perr.Path)
@@ -274,10 +428,10 @@ if errors.As(err, &pathErr) { ... } // works through wrapping
## 6. errors.Join — Multi-Error Aggregation ## 6. errors.Join — Multi-Error Aggregation
### Source: `src/errors/join.go:20-39` ### Source: [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
```go ```go
// src/errors/join.go:20-39 // [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
func Join(errs ...error) error { func Join(errs ...error) error {
n := 0 n := 0
for _, err := range errs { for _, err := range errs {
@@ -313,6 +467,77 @@ errs = append(errs, closeCache())
return errors.Join(errs...) // nil if all nil return errors.Join(errs...) // nil if all nil
``` ```
### When to Use
**Triggers:**
- You're closing/cleaning up multiple resources and each can fail independently
- A validation function checks multiple fields and you want ALL errors, not just the first
- You're running parallel operations and collecting errors from each
**Example — before:**
```go
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
if err := db.Close(); err != nil {
return err // stops here — cache and file leak!
}
if err := cache.Close(); err != nil {
return err // file still leaks
}
return file.Close()
}
```
**Example — after:**
```go
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
return errors.Join(
db.Close(),
cache.Close(),
file.Close(),
) // nil if all nil; contains all failures otherwise
}
```
### When NOT to Use
**Don't use this when:**
- Errors are causally related (error B was caused by error A) — use wrapping (`%w`) instead
- You're collecting errors across retries of the *same* operation — return the last error or wrap them causally
- The caller needs to distinguish which resource failed — Join loses that information
**Over-application example:**
```go
func fetchWithRetry(url string, attempts int) error {
var errs []error
for i := 0; i < attempts; i++ {
err := fetch(url)
if err == nil {
return nil
}
errs = append(errs, err) // collecting retry errors — misleading
}
return errors.Join(errs...) // caller sees 3 errors but they're the SAME operation
}
```
**Better alternative:**
```go
func fetchWithRetry(url string, attempts int) error {
var lastErr error
for i := 0; i < attempts; i++ {
lastErr = fetch(url)
if lastErr == nil {
return nil
}
}
return fmt.Errorf("fetch %s: %d attempts failed, last: %w", url, attempts, lastErr)
}
```
**Why:** `errors.Join` is for *independent* failures that all matter equally. For retries, the caller cares about the final failure and the retry count, not a list of (often identical) errors. For causal chains, wrapping preserves the relationship between cause and effect.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -330,7 +555,7 @@ return lastErr
## 7. Custom Is() Method — Equivalence Classes ## 7. Custom Is() Method — Equivalence Classes
### Source: `src/errors/wrap.go:42-44` (doc comment), `src/context/context.go:177-179` ### Source: [src/errors/wrap.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L42) (doc comment), [src/context/context.go#L177](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L177)
From the `errors.Is` doc: From the `errors.Is` doc:
```go ```go
@@ -344,7 +569,7 @@ From the `errors.Is` doc:
Real example from context: Real example from context:
```go ```go
// src/context/context.go:177-179 // [src/context/context.go#L177](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L177)
type deadlineExceededError struct{} type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" } func (deadlineExceededError) Error() string { return "context deadline exceeded" }
@@ -369,10 +594,10 @@ func (e MyError) Is(target error) bool {
## 8. Error Wrapping in Custom Types (Unwrap pattern) ## 8. Error Wrapping in Custom Types (Unwrap pattern)
### Source: `src/encoding/json/encode.go:276-293` ### Source: [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
```go ```go
// src/encoding/json/encode.go:276-282 // [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
type MarshalerError struct { type MarshalerError struct {
Type reflect.Type Type reflect.Type
Err error Err error
@@ -425,10 +650,10 @@ func (e *MyError) Error() string { return e.Err.Error() }
## 9. ErrUnsupported — Feature Detection via Errors ## 9. ErrUnsupported — Feature Detection via Errors
### Source: `src/errors/errors.go:76-83` ### Source: [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
```go ```go
// src/errors/errors.go:76-83 // [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
// ErrUnsupported indicates that a requested operation cannot be performed, // ErrUnsupported indicates that a requested operation cannot be performed,
// because it is unsupported. // because it is unsupported.
// //
@@ -460,10 +685,10 @@ return errors.ErrUnsupported // no info about what operation or why
## 10. Error String Conventions ## 10. Error String Conventions
### Source: `src/net/http/server.go:39-56` ### Source: [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
```go ```go
// src/net/http/server.go:39-56 // [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
var ( var (
ErrHijacked = errors.New("http: connection has been hijacked") ErrHijacked = errors.New("http: connection has been hijacked")
ErrContentLength = errors.New("http: wrote more than the declared Content-Length") ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
@@ -517,3 +742,5 @@ Is this a specific, well-known condition?
| Aggregate multiple errors | `errors.Join(err1, err2)` | | Aggregate multiple errors | `errors.Join(err1, err2)` |
| Make custom types traversable | Implement `Unwrap() error` | | Make custom types traversable | Implement `Unwrap() error` |
| Define error equivalence | Implement `Is(error) bool` | | Define error equivalence | Implement `Is(error) bool` |
<!-- PATTERN_COMPLETE -->
+318 -28
View File
@@ -6,22 +6,22 @@ Patterns extracted from the Go standard library source code.
## 1. Small Interfaces (1-2 Methods) ## 1. Small Interfaces (1-2 Methods)
### Source: `src/io/io.go:80-92` (Reader), `93-103` (Writer), `105-109` (Closer) ### Source: [src/io/io.go#L80](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L80) (Reader), `93-103` (Writer), `105-109` (Closer)
Go's most powerful interfaces have exactly **one method**: Go's most powerful interfaces have exactly **one method**:
```go ```go
// src/io/io.go:80-92 // [src/io/io.go#L80](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L80)
type Reader interface { type Reader interface {
Read(p []byte) (n int, err error) Read(p []byte) (n int, err error)
} }
// src/io/io.go:93-103 // [src/io/io.go#L93](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L93)
type Writer interface { type Writer interface {
Write(p []byte) (n int, err error) Write(p []byte) (n int, err error)
} }
// src/io/io.go:105-109 // [src/io/io.go#L105](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L105)
type Closer interface { type Closer interface {
Close() error Close() error
} }
@@ -31,6 +31,74 @@ type Closer interface {
Small interfaces are easy to implement and easy to compose. Any type can satisfy `io.Reader` by implementing a single method. This maximizes the number of types that can participate in the ecosystem — files, network connections, buffers, compressors, encryptors all satisfy `Reader`. Small interfaces are easy to implement and easy to compose. Any type can satisfy `io.Reader` by implementing a single method. This maximizes the number of types that can participate in the ecosystem — files, network connections, buffers, compressors, encryptors all satisfy `Reader`.
### When to Use
**Triggers:**
- You're defining a function that only needs ONE capability from its argument (reading, writing, closing)
- You want maximum reusability — many different types should be able to satisfy your requirement
- You're tempted to create a big interface but realize most consumers only use 1-2 methods
**Example — before:**
```go
// Accepts only *os.File — can't use with buffers, HTTP bodies, test mocks
func countLines(f *os.File) (int, error) {
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
**Example — after:**
```go
// Accepts io.Reader — works with files, HTTP bodies, strings.NewReader, gzip.Reader, etc.
func countLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
### When NOT to Use
**Don't use this when:**
- You only have one implementation and no tests — you're adding indirection for no reason
- The function genuinely needs multiple capabilities together (reading + seeking + closing)
- You're creating an interface to match a single concrete type that you control
**Over-application example:**
```go
// Interface with one implementation, no tests, no external consumers
type Configurer interface {
LoadConfig(path string) (*Config, error)
}
type fileConfigurer struct{}
func (f *fileConfigurer) LoadConfig(path string) (*Config, error) {
return parseFile(path)
}
func NewApp(c Configurer) *App {
// c is always *fileConfigurer — the interface adds nothing
return &App{cfg: c}
}
```
**Better alternative:**
```go
// Just use the concrete type until you actually need the abstraction
func NewApp(cfgPath string) *App {
cfg := parseFile(cfgPath)
return &App{cfg: cfg}
}
```
**Why:** Interfaces in Go should be discovered through usage, not predicted. "Accept interfaces" means accept them at the *boundaries* where multiple types actually flow through. If you have one implementation and no tests that need a mock, you have a premature abstraction.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -52,30 +120,30 @@ Large interfaces are hard to implement, hard to mock, and couple consumers to ca
## 2. Interface Composition ## 2. Interface Composition
### Source: `src/io/io.go:131-155` ### Source: [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
Compose small interfaces into larger ones only when needed: Compose small interfaces into larger ones only when needed:
```go ```go
// src/io/io.go:131-134 // [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
type ReadWriter interface { type ReadWriter interface {
Reader Reader
Writer Writer
} }
// src/io/io.go:136-139 // [src/io/io.go#L136](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L136)
type ReadCloser interface { type ReadCloser interface {
Reader Reader
Closer Closer
} }
// src/io/io.go:141-144 // [src/io/io.go#L141](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L141)
type WriteCloser interface { type WriteCloser interface {
Writer Writer
Closer Closer
} }
// src/io/io.go:146-150 // [src/io/io.go#L146](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L146)
type ReadWriteCloser interface { type ReadWriteCloser interface {
Reader Reader
Writer Writer
@@ -100,13 +168,13 @@ func processData(rw ReadWriteCloser) {
## 3. Accept Interfaces, Return Structs ## 3. Accept Interfaces, Return Structs
### Source: `src/io/io.go:461` (LimitReader), `src/io/io.go:618` (TeeReader) ### Source: [src/io/io.go#L461](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L461) (LimitReader), [src/io/io.go#L618](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L618) (TeeReader)
```go ```go
// src/io/io.go:461 // src/io/io.go:461
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} } func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// src/io/io.go:467-471 // [src/io/io.go#L467](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L467)
type LimitedReader struct { type LimitedReader struct {
R Reader // underlying reader R Reader // underlying reader
N int64 // max bytes remaining N int64 // max bytes remaining
@@ -114,7 +182,7 @@ type LimitedReader struct {
``` ```
```go ```go
// src/io/io.go:618-620 // [src/io/io.go#L618](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L618)
func TeeReader(r Reader, w Writer) Reader { func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w} return &teeReader{r, w}
} }
@@ -127,6 +195,69 @@ func TeeReader(r Reader, w Writer) Reader {
The return type of `LimitReader` is `Reader` (interface), but the underlying value is `*LimitedReader` (struct). Functions like `io.Copy` can type-assert to `*LimitedReader` to optimize buffer sizes (line 425). The return type of `LimitReader` is `Reader` (interface), but the underlying value is `*LimitedReader` (struct). Functions like `io.Copy` can type-assert to `*LimitedReader` to optimize buffer sizes (line 425).
### When to Use
**Triggers:**
- You're writing a function/constructor that operates on a capability (reading, hashing, connecting)
- Your return type has useful fields or methods beyond the interface contract
- You want callers to pass anything that satisfies the contract, but return something concrete they can inspect
**Example — before:**
```go
// Too restrictive input, too vague output
func NewLogger(f *os.File) io.Writer {
return &logger{out: f, level: "info"} // hides SetLevel, Flush methods
}
```
**Example — after:**
```go
type Logger struct {
out io.Writer
level string
}
func (l *Logger) SetLevel(lvl string) { l.level = lvl }
func (l *Logger) Flush() error { /* ... */ }
// Accept interface (any io.Writer), return struct (full access)
func NewLogger(w io.Writer) *Logger {
return &Logger{out: w, level: "info"}
}
```
### When NOT to Use
**Don't use this when:**
- Your function is internal and only ever called with one concrete type
- Returning an interface is genuinely better because the concrete type is an implementation detail that may change
- The struct's exported fields would expose dangerous internals
**Over-application example:**
```go
// Accepting an interface when only one concrete type makes sense
func NewDatabaseMigrator(db interface {
Exec(query string, args ...any) (sql.Result, error)
Query(query string, args ...any) (*sql.Rows, error)
Begin() (*sql.Tx, error)
}) *Migrator {
// This custom interface exactly matches *sql.DB — just accept *sql.DB
return &Migrator{db: db}
}
```
**Better alternative:**
```go
// Accept the concrete type when the abstraction doesn't buy anything
func NewDatabaseMigrator(db *sql.DB) *Migrator {
return &Migrator{db: db}
}
```
**Why:** "Accept interfaces" doesn't mean "always accept interfaces." If you define a bespoke interface that matches exactly one concrete type and no one else will implement it, you've just added indirection. The guideline targets *standard* interfaces (io.Reader, io.Writer) that many types satisfy.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -141,7 +272,7 @@ func NewServer() ServerInterface // hides useful config fields
## 4. Interface Satisfaction as a Compile-Time Check ## 4. Interface Satisfaction as a Compile-Time Check
### Source: `src/io/io.go:645`, `src/net/http/server.go:4071` ### Source: [src/io/io.go#L645](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L645), [src/net/http/server.go#L4071](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4071)
```go ```go
// src/io/io.go:645 // src/io/io.go:645
@@ -168,10 +299,10 @@ func doSomething(w ResponseWriter) {
## 5. Interface-Based Polymorphism (sort.Interface) ## 5. Interface-Based Polymorphism (sort.Interface)
### Source: `src/sort/sort.go:16-41` ### Source: [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
```go ```go
// src/sort/sort.go:16-41 // [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
type Interface interface { type Interface interface {
Len() int Len() int
Less(i, j int) bool Less(i, j int) bool
@@ -197,17 +328,17 @@ Note: Since Go 1.21, `slices.SortFunc` is preferred for slices (generic + faster
## 6. The Adapter Pattern (HandlerFunc) ## 6. The Adapter Pattern (HandlerFunc)
### Source: `src/net/http/server.go:2334-2342` ### Source: [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
```go ```go
// src/net/http/server.go:2334-2338 // [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
// The HandlerFunc type is an adapter to allow the use of // The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function // ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a // with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f. // Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request) type HandlerFunc func(ResponseWriter, *Request)
// src/net/http/server.go:2341-2342 // [src/net/http/server.go#L2341](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2341)
// ServeHTTP calls f(w, r). // ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r) f(w, r)
@@ -218,6 +349,78 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
This bridges functions and interfaces. Any function with the right signature becomes a `Handler` via `HandlerFunc(myFunc)`. You get the simplicity of functions with the composability of interfaces. This bridges functions and interfaces. Any function with the right signature becomes a `Handler` via `HandlerFunc(myFunc)`. You get the simplicity of functions with the composability of interfaces.
### When to Use
**Triggers:**
- You have an interface with a single method and users frequently implement it with a bare function
- You want to accept both struct-based and function-based implementations of the same behavior
- Requiring a struct definition for simple cases feels like boilerplate
**Example — before:**
```go
type Processor interface {
Process(data []byte) error
}
// User must create a whole struct just to use a function
type upperProcessor struct{}
func (u upperProcessor) Process(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}
```
**Example — after:**
```go
type Processor interface {
Process(data []byte) error
}
// Adapter: any function with the right signature becomes a Processor
type ProcessorFunc func([]byte) error
func (f ProcessorFunc) Process(data []byte) error { return f(data) }
// Now users can write:
pipeline.Use(ProcessorFunc(func(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}))
```
### When NOT to Use
**Don't use this when:**
- The interface has more than one method — adapters only work for single-method interfaces
- Implementations typically need state (struct fields) that closures would awkwardly close over
- The function signature is complex enough that a named type with methods is clearer
**Over-application example:**
```go
// Adapter for a multi-method interface — doesn't work
type StorageFunc func(key string, data []byte) error
func (f StorageFunc) Store(key string, data []byte) error { return f(key, data) }
func (f StorageFunc) Load(key string) ([]byte, error) { /* can't implement! */ }
func (f StorageFunc) Delete(key string) error { /* can't implement! */ }
```
**Better alternative:**
```go
// For multi-method interfaces, use a struct (or split the interface)
type MemoryStorage struct {
data map[string][]byte
}
func (m *MemoryStorage) Store(key string, data []byte) error { ... }
func (m *MemoryStorage) Load(key string) ([]byte, error) { ... }
func (m *MemoryStorage) Delete(key string) error { ... }
```
**Why:** The adapter pattern bridges functions to interfaces. Functions have one signature, so adapters only work for single-method interfaces. If your interface has multiple methods, callers need a struct anyway — the adapter just adds confusion.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -233,15 +436,15 @@ func (h myHandler) ServeHTTP(w ResponseWriter, r *Request) {
## 7. Optional Interfaces (Runtime Feature Detection) ## 7. Optional Interfaces (Runtime Feature Detection)
### Source: `src/net/http/server.go:165-175` (Flusher), `src/net/http/server.go:183-206` (Hijacker) ### Source: [src/net/http/server.go#L165](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L165) (Flusher), [src/net/http/server.go#L183](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L183) (Hijacker)
```go ```go
// src/net/http/server.go:165-170 // [src/net/http/server.go#L165](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L165)
type Flusher interface { type Flusher interface {
Flush() Flush()
} }
// src/net/http/server.go:183-206 // [src/net/http/server.go#L183](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L183)
type Hijacker interface { type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error) Hijack() (net.Conn, *bufio.ReadWriter, error)
} }
@@ -259,6 +462,91 @@ if flusher, ok := w.(Flusher); ok {
Not every `ResponseWriter` supports flushing or hijacking (HTTP/2 doesn't support Hijacker). Instead of bloating the main interface, optional capabilities are separate interfaces checked at runtime. This keeps the core interface small while allowing progressive enhancement. Not every `ResponseWriter` supports flushing or hijacking (HTTP/2 doesn't support Hijacker). Instead of bloating the main interface, optional capabilities are separate interfaces checked at runtime. This keeps the core interface small while allowing progressive enhancement.
### When to Use
**Triggers:**
- Some implementations support a capability but others don't (flushing, hijacking, seeking)
- You want to keep the core interface small but allow optimizations when available
- You're writing middleware that should enhance behavior when possible, not require it
**Example — before:**
```go
// Forces ALL stores to implement caching, even simple ones
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
InvalidateCache() error // not all stores have a cache!
}
```
**Example — after:**
```go
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
}
// Optional capability — check at runtime
type Cacheable interface {
InvalidateCache() error
}
func refreshAll(s Store) {
if c, ok := s.(Cacheable); ok {
c.InvalidateCache() // only if supported
}
}
```
### When NOT to Use
**Don't use this when:**
- All implementations will always support the capability — just put it in the main interface
- The capability is required for correctness, not just optimization
- You have only 2-3 implementations and a simple interface split handles it better
**Over-application example:**
```go
// Every HTTP handler MUST write a response — this isn't optional
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
// Don't make response-writing "optional"
type ResponseWriter interface {
Header() Header
}
type BodyWriter interface {
Write([]byte) (int, error) // NOT optional — every response needs a body mechanism
}
func handle(w ResponseWriter) {
if bw, ok := w.(BodyWriter); ok { // wrong: Write is fundamental, not optional
bw.Write([]byte("hello"))
}
}
```
**Better alternative:**
```go
// Write is fundamental — keep it in the core interface
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
// Only truly optional capabilities get separate interfaces
type Flusher interface {
Flush()
}
```
**Why:** Optional interfaces are for progressive enhancement — capabilities that some implementations support but others legitimately don't. If every implementation must support it for the system to work, it belongs in the core interface. Overusing type assertions makes code fragile and harder to reason about.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -276,10 +564,10 @@ type ResponseWriter interface {
## 8. The Stringer Interface (Convention-Based Behavior) ## 8. The Stringer Interface (Convention-Based Behavior)
### Source: `src/fmt/print.go:63-66` ### Source: [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
```go ```go
// src/fmt/print.go:63-66 // [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
type Stringer interface { type Stringer interface {
String() string String() string
} }
@@ -308,10 +596,10 @@ func printThing(v any) string {
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy) ## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
### Source: `src/io/io.go:410-417` ### Source: [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
```go ```go
// src/io/io.go:410-417 // [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) { func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy. // If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy. // Avoids an allocation and a copy.
@@ -344,15 +632,15 @@ func Copy(dst Writer, src Reader) {
## 10. The driver.Driver Pattern (Plugin Interfaces) ## 10. The driver.Driver Pattern (Plugin Interfaces)
### Source: `src/database/sql/driver/driver.go:85-97`, `104-112` ### Source: [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85), `104-112`
```go ```go
// src/database/sql/driver/driver.go:85-97 // [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85)
type Driver interface { type Driver interface {
Open(name string) (Conn, error) Open(name string) (Conn, error)
} }
// src/database/sql/driver/driver.go:104-112 // [src/database/sql/driver/driver.go#L104](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L104)
type DriverContext interface { type DriverContext interface {
OpenConnector(name string) (Connector, error) OpenConnector(name string) (Connector, error)
} }
@@ -388,3 +676,5 @@ type Driver interface {
| Compile-time interface checks | `var _ Interface = (*Type)(nil)` | | Compile-time interface checks | `var _ Interface = (*Type)(nil)` |
| Runtime interface upgrade for optimization | `io.Copy` → `WriterTo`/`ReaderFrom` | | Runtime interface upgrade for optimization | `io.Copy` → `WriterTo`/`ReaderFrom` |
| Plugin/driver interfaces start minimal | `database/sql/driver.Driver` | | Plugin/driver interfaces start minimal | `database/sql/driver.Driver` |
<!-- PATTERN_COMPLETE -->
+195 -13
View File
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
## 1. Package-Level Documentation ## 1. Package-Level Documentation
### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57` ### Source: [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5), [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5), [src/context/context.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L5)
```go ```go
// src/io/io.go:5-13 // [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5)
// Package io provides basic interfaces to I/O primitives. // Package io provides basic interfaces to I/O primitives.
// Its primary job is to wrap existing implementations of such primitives, // Its primary job is to wrap existing implementations of such primitives,
// such as those in package os, into shared public interfaces that // such as those in package os, into shared public interfaces that
@@ -22,7 +22,7 @@ package io
``` ```
```go ```go
// src/sync/mutex.go:5-11 // [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5)
// Package sync provides basic synchronization primitives such as mutual // Package sync provides basic synchronization primitives such as mutual
// exclusion locks. Other than the Once and WaitGroup types, most are intended // exclusion locks. Other than the Once and WaitGroup types, most are intended
// for use by low-level library routines. Higher-level synchronization is // for use by low-level library routines. Higher-level synchronization is
@@ -117,6 +117,58 @@ Packages under `internal/` can only be imported by code rooted at the parent of
- `net/http/internal/ascii` → importable by `net/http` and children - `net/http/internal/ascii` → importable by `net/http` and children
- NOT importable by `net/url` or any other package - NOT importable by `net/url` or any other package
### When to Use
**Triggers:**
- You have helper code shared between sub-packages but NOT part of your public API
- You're tempted to export a function "just for testing" — put it in `internal/` instead
- Your package has grown and you want to split it without committing to new public APIs
**Example — before:**
```go
// pkg/mylib/helpers.go — exported just so pkg/mylib/sub can use it
package mylib
func ParseInternalFormat(s string) Thing { ... } // now anyone can depend on this!
```
**Example — after:**
```go
// pkg/mylib/internal/parse/parse.go
package parse
func InternalFormat(s string) Thing { ... } // only importable by pkg/mylib and children
// pkg/mylib/sub/handler.go
import "pkg/mylib/internal/parse" // ✓ allowed
```
### When NOT to Use
**Don't use `internal/` when:**
- The code is only used by a single package (just keep it unexported in that package)
- You're hiding code that *should* be public API — `internal/` isn't a staging area for "maybe later"
- You have a flat package structure with no sub-packages (no one to share with)
**Over-application example:**
```go
// pkg/mylib/internal/config/config.go
package config
// Only used by pkg/mylib itself — no sub-packages import this
func DefaultTimeout() time.Duration { return 30 * time.Second }
```
**Better alternative:**
```go
// pkg/mylib/config.go — just make it unexported in the parent package
package mylib
func defaultTimeout() time.Duration { return 30 * time.Second }
```
**Why:** `internal/` adds directory structure complexity. If you have no sub-packages sharing the code, an unexported function in the parent package is simpler and achieves the same encapsulation.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -170,7 +222,7 @@ type Parser struct {
## 5. init() Functions — Use Sparingly ## 5. init() Functions — Use Sparingly
### Source: `src/net/http/http2.go:37` ### Source: [src/net/http/http2.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/http2.go#L37)
```go ```go
// src/net/http/http2.go:37 // src/net/http/http2.go:37
@@ -192,6 +244,75 @@ The stdlib uses `init()` for:
3. Keep them short 3. Keep them short
4. Prefer explicit initialization in `main()` when possible 4. Prefer explicit initialization in `main()` when possible
### When to Use
**Triggers:**
- You're writing a driver or plugin that needs to register itself with a central registry on import
- The registration is side-effect-only (no return value, can't fail)
- You want `import _ "mydb/driver"` to make the driver available without explicit setup
**Example — before:**
```go
// main.go — user must manually register every driver
func main() {
postgres.Register() // easy to forget
mysql.Register() // order matters?
sqlite.Register()
}
```
**Example — after:**
```go
// postgres/driver.go
func init() {
sql.Register("postgres", &Driver{}) // auto-registers on import
}
// main.go — import for side-effect
import _ "github.com/lib/pq" // driver registers itself
```
### When NOT to Use
**Don't use `init()` when:**
- The initialization can fail (you can't return errors from `init()`)
- The setup requires configuration or parameters (init takes no args)
- You need to control initialization order across packages
- It's a one-off application (not a library/driver) — just call setup in `main()`
**Over-application example:**
```go
// internal/metrics/metrics.go
func init() {
// Bad: init() hides this dependency, makes testing impossible,
// and panics if prometheus isn't reachable
prometheus.MustRegister(requestCounter)
prometheus.MustRegister(errorCounter)
prometheus.MustRegister(latencyHistogram)
}
```
**Better alternative:**
```go
// internal/metrics/metrics.go
func Register(reg prometheus.Registerer) error {
if err := reg.Register(requestCounter); err != nil {
return fmt.Errorf("registering request counter: %w", err)
}
// ...
return nil
}
// main.go
func main() {
if err := metrics.Register(prometheus.DefaultRegisterer); err != nil {
log.Fatal(err)
}
}
```
**Why:** `init()` is invisible, untestable, and can't fail gracefully. Use it only when the registration pattern demands it (database/sql drivers, codec registration) and failure is impossible.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -258,15 +379,15 @@ srv := &http.Server{
## 7. Constructor Pattern — NewX Functions ## 7. Constructor Pattern — NewX Functions
### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836` ### Source: [src/net/http/server.go#L2638](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2638), [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
```go ```go
// src/net/http/server.go:2639 // src/net/http/server.go:2638
func NewServeMux() *ServeMux { func NewServeMux() *ServeMux {
return new(ServeMux) return &ServeMux{}
} }
// src/database/sql/sql.go:836-843 // [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
func OpenDB(c driver.Connector) *DB { func OpenDB(c driver.Connector) *DB {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
db := &DB{ db := &DB{
@@ -367,10 +488,10 @@ The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic.
## 10. Context Key Pattern — Type-Safe Context Values ## 10. Context Key Pattern — Type-Safe Context Values
### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252` ### Source: [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132), [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
```go ```go
// src/context/context.go:132-164 (from doc) // [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132) (from doc)
// package user // package user
// //
// type key int // type key int
@@ -387,7 +508,7 @@ The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic.
``` ```
```go ```go
// src/net/http/server.go:244-252 // [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
var ( var (
ServerContextKey = &contextKey{"http-server"} ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"} LocalAddrContextKey = &contextKey{"local-addr"}
@@ -404,6 +525,65 @@ type contextKey struct {
- **Type-safe accessors** avoid repeated type assertions - **Type-safe accessors** avoid repeated type assertions
- **Pointer-based keys** guarantee uniqueness - **Pointer-based keys** guarantee uniqueness
### When to Use
**Triggers:**
- You need to pass request-scoped metadata through a call chain (user ID, trace ID, auth token)
- The data crosses package boundaries and isn't appropriate as a function parameter
- You want type safety — only your package should read/write its context values
**Example — before:**
```go
// String keys — any package can collide or access your values
ctx = context.WithValue(ctx, "userID", 42)
uid := ctx.Value("userID").(int) // panics if wrong type or missing
```
**Example — after:**
```go
type ctxKey struct{}
func WithUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, ctxKey{}, id)
}
func UserID(ctx context.Context) (int, bool) {
id, ok := ctx.Value(ctxKey{}).(int)
return id, ok
}
```
### When NOT to Use
**Don't use context values when:**
- The data is a required function parameter (pass it explicitly)
- The data controls behavior/logic (timeouts, retry counts) — use function args or config structs
- You're using it to avoid refactoring function signatures
- The value is large or expensive to retrieve (context isn't a cache)
**Over-application example:**
```go
// Passing database connection through context — it's required everywhere!
func HandleRequest(ctx context.Context) {
db := DatabaseFromContext(ctx) // nil if forgotten — runtime panic
users, err := db.Query(ctx, "SELECT ...")
}
```
**Better alternative:**
```go
// Make the dependency explicit
type Handler struct {
db *sql.DB
}
func (h *Handler) HandleRequest(ctx context.Context) {
users, err := h.db.QueryContext(ctx, "SELECT ...")
}
```
**Why:** Context values are untyped, invisible in function signatures, and can silently be nil. They're meant for *request-scoped metadata* that crosses API boundaries (trace IDs, auth tokens), not for dependency injection or configuration.
### Anti-pattern ### Anti-pattern
```go ```go
@@ -418,10 +598,10 @@ ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
## 11. Struct Tags for Codec Configuration ## 11. Struct Tags for Codec Configuration
### Source: `src/encoding/json/tags.go:17-21`, `src/encoding/json/encode.go:101-181` ### Source: [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17), [src/encoding/json/encode.go#L101](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L101)
```go ```go
// src/encoding/json/tags.go:17-21 // [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17)
func parseTag(tag string) (string, tagOptions) { func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",") tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt) return tag, tagOptions(opt)
@@ -465,3 +645,5 @@ Struct tags are metadata for codecs. The `json` package reads `json:"..."` tags
| API layers | Separate user from implementor (SPI) | | API layers | Separate user from implementor (SPI) |
| Context values | Unexported key type + typed accessors | | Context values | Unexported key type + typed accessors |
| Configuration | Struct literals or functional options | | Configuration | Struct literals or functional options |
<!-- PATTERN_COMPLETE -->
+210 -13
View File
@@ -1,10 +1,12 @@
# Struct Design Patterns in the Go Standard Library # Struct Design Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Zero-Value Usability ## 1. Zero-Value Usability
**Pattern name:** Zero Value Ready **Pattern name:** Zero Value Ready
**Source citation:** `net/http/client.go` lines 31–35, `strings/builder.go` lines 14–16 **Source citation:** [net/http/client.go#L31](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L31), [strings/builder.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L14)
**What it does:** Structs are designed so their zero value is immediately useful without **What it does:** Structs are designed so their zero value is immediately useful without
explicit initialization. Nil fields fall back to sensible defaults at method call time. explicit initialization. Nil fields fall back to sensible defaults at method call time.
@@ -13,13 +15,80 @@ explicit initialization. Nil fields fall back to sensible defaults at method cal
self-documenting about its defaults. Users can write `var c http.Client` and start self-documenting about its defaults. Users can write `var c http.Client` and start
making requests. making requests.
**When to Use**
**Triggers:**
- You're designing a type where the "empty" or "default" state is meaningful and safe
- Users should be able to write `var x MyType` and immediately call methods
- Your struct's nil/zero fields can fall back to sensible defaults at call time
**Example — before:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration
}
// Panics on zero value — store is nil!
func (c *Cache) Set(k string, v []byte) { c.store[k] = v }
```
**Example — after:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration // zero means no expiry
}
func (c *Cache) Set(k string, v []byte) {
if c.store == nil {
c.store = make(map[string][]byte) // lazy init on first use
}
c.store[k] = v
}
```
### When NOT to Use
**Don't use this when:**
- The type has mandatory dependencies that *must* be provided (a DB connection, an io.Reader with no sensible default)
- The zero value would be dangerous rather than merely useless (e.g., a zero-value security config that disables auth)
- Lazy initialization adds per-call overhead on a hot path and the constructor is called once
**Over-application example:**
```go
// Trying to make a DB-backed store zero-value ready
type UserStore struct {
db *sql.DB // nil means... what? No sensible default exists.
}
func (s *UserStore) Get(id int) (*User, error) {
if s.db == nil {
return nil, errors.New("no database configured") // every call pays for nil check
}
// ...
}
```
**Better alternative:**
```go
// A constructor makes the requirement explicit
func NewUserStore(db *sql.DB) *UserStore {
return &UserStore{db: db}
}
```
**Why:** When there's no meaningful default for a dependency, forcing zero-value usability
just moves the error from compile time (missing argument) to runtime (nil check on every call).
Use a constructor instead.
**Anti-pattern:** Requiring a constructor for basic use; panicking on zero-value use; **Anti-pattern:** Requiring a constructor for basic use; panicking on zero-value use;
requiring all fields be set before the type is functional. requiring all fields be set before the type is functional.
**Code examples from source:** **Code examples from source:**
```go ```go
// net/http/client.go:31-35 // net/http/client.go:30-34
// A Client is an HTTP client. Its zero value ([DefaultClient]) is a // A Client is an HTTP client. Its zero value ([DefaultClient]) is a
// usable client that uses [DefaultTransport]. // usable client that uses [DefaultTransport].
type Client struct { type Client struct {
@@ -59,7 +128,7 @@ type Buffer struct {
**Pattern name:** Indirection via Unexported Impl **Pattern name:** Indirection via Unexported Impl
**Source citation:** `os/types.go` lines 16–20, `os/file_unix.go` lines 59–71 **Source citation:** [os/types.go#L15](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/types.go#L15), [os/file_unix.go#L59](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file_unix.go#L59)
**What it does:** The exported type (`File`) embeds a pointer to an unexported type **What it does:** The exported type (`File`) embeds a pointer to an unexported type
(`*file`) that holds the real implementation state. Users interact only with the (`*file`) that holds the real implementation state. Users interact only with the
@@ -76,7 +145,7 @@ public API.
**Code example from source:** **Code example from source:**
```go ```go
// os/types.go:16-20 // os/types.go:15-20
// File represents an open file descriptor. // File represents an open file descriptor.
// //
// The methods of File are safe for concurrent use. // The methods of File are safe for concurrent use.
@@ -106,7 +175,7 @@ type file struct {
**Pattern name:** NewXxx Constructor **Pattern name:** NewXxx Constructor
**Source citation:** `bufio/scan.go` lines 89–96, `bufio/bufio.go` lines 50–60 **Source citation:** [bufio/scan.go#L89](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L89), [bufio/bufio.go#L50](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/bufio.go#L50)
**What it does:** A package-level function `NewXxx(deps) *Xxx` constructs the type **What it does:** A package-level function `NewXxx(deps) *Xxx` constructs the type
with required dependencies and internal defaults that can't be expressed via zero with required dependencies and internal defaults that can't be expressed via zero
@@ -116,6 +185,68 @@ value alone.
clearly communicates what's required. The constructor can set internal invariants clearly communicates what's required. The constructor can set internal invariants
(buffer sizes, split functions) that users shouldn't need to know about. (buffer sizes, split functions) that users shouldn't need to know about.
**When to Use**
**Triggers:**
- Your type has mandatory dependencies that can't be expressed as zero values (an `io.Reader`, a DB connection)
- Internal invariants must be set up (buffer allocation, goroutine start)
- The type isn't useful without initialization (unlike `sync.Mutex` or `bytes.Buffer`)
**Example — before:**
```go
type Parser struct {
lexer *Lexer
buf []Token
maxDepth int
}
// User must know about all internal state:
p := &Parser{lexer: NewLexer(input), buf: make([]Token, 0, 64), maxDepth: 100}
```
**Example — after:**
```go
func NewParser(input io.Reader) *Parser {
return &Parser{
lexer: NewLexer(input),
buf: make([]Token, 0, 64),
maxDepth: 100,
}
}
// User writes:
p := NewParser(file)
```
### When NOT to Use
**Don't use this when:**
- The type's zero value is already useful — adding a constructor creates unnecessary ceremony
- Your constructor takes 5+ optional parameters (use a config struct instead)
- The "mandatory dependency" is actually optional and has a sensible default (e.g., a logger defaulting to `slog.Default()`)
**Over-application example:**
```go
// Constructor for something that doesn't need one
func NewCounter() *Counter {
return &Counter{count: 0} // zero value already does this!
}
// Forces users to write:
c := NewCounter()
```
**Better alternative:**
```go
type Counter struct {
count int64
}
// Users write: var c Counter — done.
```
**Why:** If the zero value works, a constructor is just noise. It obscures the type's
actual simplicity and makes users wonder what hidden initialization they're missing.
**Anti-pattern:** Forcing users to manually set unexported fields; having a constructor **Anti-pattern:** Forcing users to manually set unexported fields; having a constructor
that takes 10 optional parameters (use config struct instead); requiring New when that takes 10 optional parameters (use config struct instead); requiring New when
zero value would suffice. zero value would suffice.
@@ -165,7 +296,7 @@ func NewRequest(method, url string, body io.Reader) (*Request, error) {
**Pattern name:** NewXxx / NewXxxSize Pair **Pattern name:** NewXxx / NewXxxSize Pair
**Source citation:** `bufio/bufio.go` lines 50, 62, 589, 607 **Source citation:** [bufio/bufio.go#L50](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/bufio.go#L50), 62, 589, 607
**What it does:** Provides two constructors — one with defaults (`NewReader`) and one **What it does:** Provides two constructors — one with defaults (`NewReader`) and one
with explicit configuration (`NewReaderSize`). The default version calls the with explicit configuration (`NewReaderSize`). The default version calls the
@@ -196,7 +327,7 @@ func NewWriter(w io.Writer) *Writer {
**Pattern name:** Configuration Struct (Exported Fields, Nil-Means-Default) **Pattern name:** Configuration Struct (Exported Fields, Nil-Means-Default)
**Source citation:** `net/http/server.go` lines 3020–3120, `crypto/tls/common.go` lines 566+, `log/slog/handler.go` lines 135–175 **Source citation:** [net/http/server.go#L3020](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3020), [crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566)+, [log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135)
**What it does:** A struct with exported, documented fields provides all **What it does:** A struct with exported, documented fields provides all
configuration knobs. Nil/zero values always mean "use the default". configuration knobs. Nil/zero values always mean "use the default".
@@ -205,6 +336,70 @@ configuration knobs. Nil/zero values always mean "use the default".
construct partially; serializable; the zero value works. This is Go's primary construct partially; serializable; the zero value works. This is Go's primary
configuration pattern (preferred over functional options in the stdlib). configuration pattern (preferred over functional options in the stdlib).
**When to Use**
**Triggers:**
- Your constructor has 4+ optional parameters that would make a function signature unwieldy
- You want users to see all options in one place with godoc documentation
- Zero/nil values should mean "use the default" — no required fields beyond what the constructor demands
**Example — before:**
```go
// 7 parameters — impossible to remember the order
func NewServer(addr string, handler http.Handler, readTimeout, writeTimeout time.Duration,
maxConns int, logger *log.Logger, tlsConfig *tls.Config) *Server { ... }
```
**Example — after:**
```go
type ServerConfig struct {
Addr string // ":8080" if empty
Handler http.Handler // http.DefaultServeMux if nil
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxConns int // 1000 if zero
Logger *log.Logger // log.Default() if nil
TLSConfig *tls.Config // plain HTTP if nil
}
func NewServer(cfg ServerConfig) *Server { ... }
```
### When NOT to Use
**Don't use this when:**
- You only have 1–2 options — just use function parameters
- The options are truly required (not optional) — they belong in the constructor signature
- Your config struct has methods or behavior — it's no longer a plain config, it's becoming a type
**Over-application example:**
```go
// Config struct for two required parameters
type ClientConfig struct {
BaseURL string // required!
APIKey string // required!
}
func NewClient(cfg ClientConfig) (*Client, error) {
if cfg.BaseURL == "" { return nil, errors.New("base URL required") }
if cfg.APIKey == "" { return nil, errors.New("API key required") }
// ...
}
```
**Better alternative:**
```go
// Required params are function args; only truly optional things go in a config
func NewClient(baseURL, apiKey string, opts *ClientOptions) (*Client, error) {
// baseURL and apiKey are enforced by the signature
// opts can be nil for defaults
}
```
**Why:** Config structs shine for *optional* configuration. When fields are required,
the compiler can't enforce them — you end up validating at runtime what the type system
could have caught. Keep required parameters as explicit function arguments.
**Anti-pattern:** Undocumented fields; requiring all fields set; using sentinel values **Anti-pattern:** Undocumented fields; requiring all fields set; using sentinel values
other than zero/nil for defaults; providing setters when direct assignment works. other than zero/nil for defaults; providing setters when direct assignment works.
@@ -247,7 +442,7 @@ func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
**Pattern name:** Interface Abstraction for Pluggable Implementations **Pattern name:** Interface Abstraction for Pluggable Implementations
**Source citation:** `crypto/crypto.go` lines 180–200, `net/http/transport.go` lines 66–82 **Source citation:** [crypto/crypto.go#L180](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L180), [net/http/transport.go#L66](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L66)
**What it does:** Core behavior is defined via an interface. The package provides **What it does:** Core behavior is defined via an interface. The package provides
a default concrete implementation, but any user type satisfying the interface a default concrete implementation, but any user type satisfying the interface
@@ -279,7 +474,7 @@ type Signer interface {
// Transports should be reused instead of created as needed. // Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines. // Transports are safe for concurrent use by multiple goroutines.
// net/http/client.go:59-60 // net/http/client.go:57-58
type Client struct { type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used. Transport RoundTripper // If nil, DefaultTransport is used.
// ... // ...
@@ -292,7 +487,7 @@ type Client struct {
**Pattern name:** copyCheck (Runtime Copy Detection) **Pattern name:** copyCheck (Runtime Copy Detection)
**Source citation:** `strings/builder.go` lines 25–40 **Source citation:** [strings/builder.go#L32](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L32)
**What it does:** On first mutation, the Builder records its own address. Subsequent **What it does:** On first mutation, the Builder records its own address. Subsequent
mutations compare the current receiver address against the recorded one. If they mutations compare the current receiver address against the recorded one. If they
@@ -308,7 +503,7 @@ buffer), a runtime check is the pragmatic solution.
**Code example from source:** **Code example from source:**
```go ```go
// strings/builder.go:25-40 // strings/builder.go:32-40
func (b *Builder) copyCheck() { func (b *Builder) copyCheck() {
if b.addr == nil { if b.addr == nil {
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b))) b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))
@@ -324,7 +519,7 @@ func (b *Builder) copyCheck() {
**Pattern name:** Package-Level Default Instance **Pattern name:** Package-Level Default Instance
**Source citation:** `net/http/client.go` line 109, `net/http/transport.go` lines 47–58 **Source citation:** [net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L109), [net/http/transport.go#L47](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L47)
**What it does:** The package provides a pre-configured, ready-to-use instance as **What it does:** The package provides a pre-configured, ready-to-use instance as
a package-level variable. Package-level convenience functions delegate to it. a package-level variable. Package-level convenience functions delegate to it.
@@ -364,7 +559,7 @@ var DefaultTransport RoundTripper = &Transport{
**Pattern name:** Post-Construction Configuration via Methods **Pattern name:** Post-Construction Configuration via Methods
**Source citation:** `bufio/scan.go` lines 275–293 **Source citation:** [bufio/scan.go#L275](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L275)
**What it does:** After construction with `NewScanner`, optional configuration is **What it does:** After construction with `NewScanner`, optional configuration is
applied via methods (`Split`, `Buffer`) before the first call to `Scan`. applied via methods (`Split`, `Buffer`) before the first call to `Scan`.
@@ -402,3 +597,5 @@ func (s *Scanner) Split(split SplitFunc) {
s.split = split s.split = split
} }
``` ```
<!-- PATTERN_COMPLETE -->
+198 -15
View File
@@ -1,5 +1,7 @@
# Code Style Patterns in the Go Standard Library # Code Style Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Naming Conventions: mixedCaps (No Underscores) ## 1. Naming Conventions: mixedCaps (No Underscores)
**Pattern name:** mixedCaps / MixedCaps **Pattern name:** mixedCaps / MixedCaps
@@ -34,7 +36,7 @@ const shutdownPollIntervalMax = 500 * time.Millisecond
**Pattern name:** Acronym Capitalization **Pattern name:** Acronym Capitalization
**Source citation:** `net/http/request.go` line 130 (`URL`), `net/http/server.go` line 3041 (`TLSConfig`), `encoding/json/stream.go` line 280 (`JSON`) **Source citation:** [net/http/request.go#L130](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/request.go#L130) (`URL`), [net/http/server.go#L3040](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3040) (`TLSConfig`), [encoding/json/stream.go#L280](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/stream.go#L280) (`JSON`)
**What it does:** Acronyms and initialisms (URL, HTTP, ID, JSON, XML, HTML, TLS, TCP) **What it does:** Acronyms and initialisms (URL, HTTP, ID, JSON, XML, HTML, TLS, TCP)
are always fully capitalized when exported, and fully lowercased when unexported. are always fully capitalized when exported, and fully lowercased when unexported.
@@ -53,10 +55,10 @@ URL *url.URL
// net/http/request.go:822 // net/http/request.go:822
func ParseHTTPVersion(vers string) (major, minor int, ok bool) func ParseHTTPVersion(vers string) (major, minor int, ok bool)
// net/http/server.go:3041 // net/http/server.go:3040
TLSConfig *tls.Config TLSConfig *tls.Config
// encoding/json/stream.go:280 // encoding/json/stream.go:292
var _ Marshaler = (*RawMessage)(nil) var _ Marshaler = (*RawMessage)(nil)
``` ```
@@ -101,7 +103,7 @@ pattern.go — URL pattern matching (ServeMux routing)
**Pattern name:** `var _ Interface = (*Type)(nil)` **Pattern name:** `var _ Interface = (*Type)(nil)`
**Source citation:** `io/io.go` line 645, `os/file.go` lines 747–750, `encoding/json/stream.go` lines 280–281 **Source citation:** [io/io.go#L645](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L645), [os/file.go#L747](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L747), [encoding/json/stream.go#L280](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/stream.go#L280)
**What it does:** A package-level `var _ InterfaceName = (*ConcreteType)(nil)` declares **What it does:** A package-level `var _ InterfaceName = (*ConcreteType)(nil)` declares
that the concrete type must satisfy the interface. The compiler verifies this at that the concrete type must satisfy the interface. The compiler verifies this at
@@ -110,6 +112,60 @@ build time.
**Why:** Catches interface drift at compile time without creating an instance. The **Why:** Catches interface drift at compile time without creating an instance. The
blank identifier discards the value — this is purely a static assertion. blank identifier discards the value — this is purely a static assertion.
**When to Use**
**Triggers:**
- You've defined a type that MUST satisfy an interface (implements `io.Reader`, `http.Handler`, etc.)
- You want a compile-time guarantee that catches drift when you add/remove methods
- You're writing a package with multiple types implementing the same interface
**Example — before:**
```go
type MyWriter struct{ buf bytes.Buffer }
func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }
// Months later, someone renames Write to WriteBuf...
// No compile error — only discovered at runtime when passed as io.Writer
```
**Example — after:**
```go
// Compile-time check: if MyWriter stops implementing io.Writer, this fails to build
var _ io.Writer = (*MyWriter)(nil)
type MyWriter struct{ buf bytes.Buffer }
func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }
```
**When NOT to Use**
**Don't use interface compliance checks when:**
- The type is unexported AND only used locally (the compiler catches it at the call site anyway)
- You're asserting against an interface you don't own and it changes frequently (creates churn)
- The interface is implemented implicitly and you're just cargo-culting the pattern for every type
**Over-application example:**
```go
// Every single struct gets a compliance check, even trivial unexported ones
var _ fmt.Stringer = (*internalHelper)(nil) // only used in one function in this file
var _ fmt.Stringer = (*anotherHelper)(nil) // also never passed as Stringer anywhere
```
**Better alternative:**
```go
// Skip the check for types that are only used locally — the compiler
// will catch the issue at the point of use:
func printThing(s fmt.Stringer) { fmt.Println(s.String()) }
printThing(&internalHelper{}) // compiler error here if String() is missing
// DO use it for exported types that implement external interfaces:
var _ io.ReadWriteCloser = (*MyConnection)(nil) // users depend on this contract
```
**Why:** The pattern exists for API contracts — types that *must* satisfy an interface for consumers. For unexported types used only locally, the compiler already catches mismatches at the call site. Adding the check everywhere is noise.
**Anti-pattern:** Relying on tests to catch interface conformance; skipping the check **Anti-pattern:** Relying on tests to catch interface conformance; skipping the check
and discovering the mismatch at runtime; using reflection. and discovering the mismatch at runtime; using reflection.
@@ -125,7 +181,7 @@ var _ fs.ReadFileFS = dirFS("")
var _ fs.ReadDirFS = dirFS("") var _ fs.ReadDirFS = dirFS("")
var _ fs.ReadLinkFS = dirFS("") var _ fs.ReadLinkFS = dirFS("")
// encoding/json/stream.go:280-281 // encoding/json/stream.go:292-293
var _ Marshaler = (*RawMessage)(nil) var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil) var _ Unmarshaler = (*RawMessage)(nil)
@@ -139,7 +195,7 @@ var _ Pusher = (*timeoutWriter)(nil)
**Pattern name:** Named Returns for Documentation (and Defer) **Pattern name:** Named Returns for Documentation (and Defer)
**Source citation:** `io/io.go` lines 87, 100, 314, 387; `os/file.go` lines 140, 175 **Source citation:** [io/io.go#L87](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L87), 100, 314, 387; [os/file.go#L140](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L140), 175
**What it does:** Return values are given names when the names add documentary value **What it does:** Return values are given names when the names add documentary value
(clarifying which int is what) or when `defer` needs to modify the return value. (clarifying which int is what) or when `defer` needs to modify the return value.
@@ -185,7 +241,7 @@ func (f *File) Read(b []byte) (n int, err error) {
**Pattern name:** `defer mu.Unlock()` / `defer f.Close()` **Pattern name:** `defer mu.Unlock()` / `defer f.Close()`
**Source citation:** `net/http/server.go` lines 3173–3174, `net/http/example_handle_test.go` lines 21–22 **Source citation:** [net/http/server.go#L3173](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3173), [net/http/example_handle_test.go#L21](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L21)
**What it does:** Resources acquired at the top of a scope are immediately deferred **What it does:** Resources acquired at the top of a scope are immediately deferred
for cleanup. Mutexes are locked then immediately `defer Unlock()`'d. for cleanup. Mutexes are locked then immediately `defer Unlock()`'d.
@@ -200,7 +256,7 @@ run earlier.
**Code examples from source:** **Code examples from source:**
```go ```go
// net/http/server.go:3173-3174 // net/http/server.go:3171-3174
func (s *Server) Close() error { func (s *Server) Close() error {
s.inShutdown.Store(true) s.inShutdown.Store(true)
s.mu.Lock() s.mu.Lock()
@@ -223,7 +279,7 @@ func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
**Pattern name:** Sentinel Errors + Structured Error Types **Pattern name:** Sentinel Errors + Structured Error Types
**Source citation:** `os/error.go` lines 14–27, `os/error.go` lines 46–67 **Source citation:** [os/error.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/error.go#L14), [os/error.go#L46](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/error.go#L46)
**What it does:** Package-level sentinel errors (`ErrNotExist`, `ErrPermission`) are **What it does:** Package-level sentinel errors (`ErrNotExist`, `ErrPermission`) are
declared as `var` for use with `errors.Is()`. Structured error types (`*PathError`, declared as `var` for use with `errors.Is()`. Structured error types (`*PathError`,
@@ -303,7 +359,7 @@ func (f *File) Name() string { ... }
**Pattern name:** Typed Constants with iota **Pattern name:** Typed Constants with iota
**Source citation:** `crypto/crypto.go` lines 70–85, `time/time.go` lines 936–943 **Source citation:** [crypto/crypto.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L70), [time/time.go#L936](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L936)
**What it does:** Related constants are grouped in a `const ( ... )` block using **What it does:** Related constants are grouped in a `const ( ... )` block using
a named type and `iota` for sequential values. Constants of the same type a named type and `iota` for sequential values. Constants of the same type
@@ -312,6 +368,70 @@ are exhaustively listed together.
**Why:** Type safety (can't accidentally pass an `os.Flag` where a `crypto.Hash` is **Why:** Type safety (can't accidentally pass an `os.Flag` where a `crypto.Hash` is
expected). `iota` eliminates magic numbers. Grouping makes the full set visible. expected). `iota` eliminates magic numbers. Grouping makes the full set visible.
**When to Use**
**Triggers:**
- You have a set of related values that represent distinct states or options (status codes, modes, categories)
- Raw integers would be meaningless to readers (`SetMode(3)` vs `SetMode(ModeAsync)`)
- You want the type system to prevent passing a "color" where a "direction" is expected
**Example — before:**
```go
func SetLogLevel(level int) { ... }
// Caller:
SetLogLevel(3) // what does 3 mean?!
SetLogLevel(-1) // valid? who knows
```
**Example — after:**
```go
type LogLevel int
const (
LevelDebug LogLevel = iota
LevelInfo
LevelWarn
LevelError
)
func SetLogLevel(level LogLevel) { ... }
// Caller:
SetLogLevel(LevelWarn) // self-documenting
```
**When NOT to Use**
**Don't use typed constants with iota when:**
- The values have external meaning (HTTP status codes, exit codes, protocol bytes) — use explicit values
- The set is not exhaustive or will have gaps (iota assigns sequential values; gaps require explicit assignment)
- You need the constant to interoperate with untyped int APIs without casting everywhere
**Over-application example:**
```go
type HTTPStatus int
const (
StatusOK HTTPStatus = iota // 0?! HTTP 200 is not 0
StatusNotFound // 1?! Should be 404
StatusServerError // 2?! Should be 500
)
```
**Better alternative:**
```go
type HTTPStatus int
const (
StatusOK HTTPStatus = 200
StatusNotFound HTTPStatus = 404
StatusServerError HTTPStatus = 500
)
```
**Why:** `iota` is for sequential enumerations where the actual numeric value doesn't matter (only the distinctness matters). When values have external meaning (wire protocols, HTTP, exit codes), use explicit values.
**Anti-pattern:** Untyped numeric constants; separate `const` declarations for related **Anti-pattern:** Untyped numeric constants; separate `const` declarations for related
values; using raw integers in function signatures. values; using raw integers in function signatures.
@@ -328,7 +448,7 @@ const (
// ... // ...
) )
// time/time.go:936-943 // time/time.go:934-942
const ( const (
Nanosecond Duration = 1 Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond Microsecond = 1000 * Nanosecond
@@ -345,7 +465,7 @@ const (
**Pattern name:** `// guards x` Field Comments **Pattern name:** `// guards x` Field Comments
**Source citation:** `net/http/example_handle_test.go` line 16 **Source citation:** [net/http/example_handle_test.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L16)
**What it does:** When a sync primitive (mutex) protects specific fields, a brief **What it does:** When a sync primitive (mutex) protects specific fields, a brief
comment documents what it guards: `mu sync.Mutex // guards n`. comment documents what it guards: `mu sync.Mutex // guards n`.
@@ -372,7 +492,7 @@ type countHandler struct {
**Pattern name:** Named Type for Semantic Units **Pattern name:** Named Type for Semantic Units
**Source citation:** `time/time.go` lines 915–943 **Source citation:** [time/time.go#L915](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L915)
**What it does:** `Duration` is `type Duration int64` — a named type over a primitive. **What it does:** `Duration` is `type Duration int64` — a named type over a primitive.
This gives it its own method set (`String()`, `Hours()`, `Truncate()`) and prevents This gives it its own method set (`String()`, `Hours()`, `Truncate()`) and prevents
@@ -382,6 +502,67 @@ accidental mixing with raw int64 values.
nanoseconds where seconds are expected. Methods provide conversion and formatting. nanoseconds where seconds are expected. Methods provide conversion and formatting.
Constants like `time.Second` make intent clear. Constants like `time.Second` make intent clear.
**When to Use**
**Triggers:**
- A primitive type (`int`, `string`, `float64`) has a specific **semantic meaning** in your domain
- You want to attach methods (formatting, validation, arithmetic) to the value
- You've seen bugs from accidentally mixing units (`int` could be seconds, milliseconds, or nanoseconds)
**Example — before:**
```go
func SetTimeout(ms int) { ... } // is this milliseconds? seconds?
func SetRetries(n int) { ... } // can't accidentally swap these... or CAN you?
SetTimeout(5) // 5 what?
SetRetries(500) // oops, swapped arguments — compiles fine!
```
**Example — after:**
```go
type Timeout time.Duration
type RetryCount int
func SetTimeout(t Timeout) { ... }
func SetRetries(n RetryCount) { ... }
SetTimeout(Timeout(5 * time.Second)) // explicit units
SetRetries(RetryCount(3)) // can't swap — different types
```
**When NOT to Use**
**Don't create named types when:**
- The primitive is only used in one place (over-engineering for a single call site)
- The type would need constant casting back to the primitive for stdlib interop
- The semantic meaning is already clear from the parameter name (`func Sleep(seconds int)` in a script is fine)
**Over-application example:**
```go
type Port uint16
type Host string
type Path string
// Now every function that takes these needs explicit construction:
func Connect(h Host, p Port, path Path) { ... }
Connect(Host("localhost"), Port(8080), Path("/api")) // ceremony for no safety gain
```
**Better alternative:**
```go
// For simple configurations, a struct with named fields provides clarity without type ceremony:
type Endpoint struct {
Host string
Port uint16
Path string
}
func Connect(ep Endpoint) { ... }
Connect(Endpoint{Host: "localhost", Port: 8080, Path: "/api"})
```
**Why:** Named types shine when you need methods, when confusion between units causes real bugs (seconds vs milliseconds), or when the type system should prevent mixing semantically different values of the same primitive. If you're just adding type annotations to strings that don't interact, you're adding ceremony without safety.
**Anti-pattern:** Using raw `int64` for durations; accepting `int` parameters for **Anti-pattern:** Using raw `int64` for durations; accepting `int` parameters for
time intervals; mixing units (milliseconds in one place, seconds in another). time intervals; mixing units (milliseconds in one place, seconds in another).
@@ -432,7 +613,7 @@ checking in code that `gofmt` would modify.
**Pattern name:** Grouped Imports (stdlib / external / internal) **Pattern name:** Grouped Imports (stdlib / external / internal)
**Source citation:** `net/http/server.go` lines 8–36 **Source citation:** [net/http/server.go#L9](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L9)
**What it does:** Imports are organized in groups separated by blank lines: **What it does:** Imports are organized in groups separated by blank lines:
1. Standard library 1. Standard library
@@ -447,7 +628,7 @@ external). Reduces merge conflicts.
**Code example from source:** **Code example from source:**
```go ```go
// net/http/server.go:8-36 // net/http/server.go:9-36
import ( import (
"bufio" "bufio"
"bytes" "bytes"
@@ -462,3 +643,5 @@ import (
"golang.org/x/net/http/httpguts" "golang.org/x/net/http/httpguts"
) )
``` ```
<!-- PATTERN_COMPLETE -->
+264 -14
View File
@@ -1,5 +1,7 @@
# Advanced Go Testing Patterns # Advanced Go Testing Patterns
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
Patterns extracted from the Go standard library (`src/net/http/`, `src/encoding/json/`, `src/testing/`) and Kubernetes source code. Patterns extracted from the Go standard library (`src/net/http/`, `src/encoding/json/`, `src/testing/`) and Kubernetes source code.
--- ---
@@ -10,12 +12,104 @@ The canonical Go test style. Every Go stdlib test file uses this pattern.
### Pattern Name: Anonymous Struct Test Table ### Pattern Name: Anonymous Struct Test Table
**Source:** `/tmp/go-src/src/net/http/header_test.go` lines 17-108 **Source:** [src/net/http/header_test.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/header_test.go#L17)
**What they do:** Define test cases as a slice of anonymous structs, iterate with a range loop. **What they do:** Define test cases as a slice of anonymous structs, iterate with a range loop.
**Why:** Eliminates repetition, makes adding cases trivial, keeps the assertion logic in one place. Every test case gets the same verification path — no "special" cases hidden in different code paths. **Why:** Eliminates repetition, makes adding cases trivial, keeps the assertion logic in one place. Every test case gets the same verification path — no "special" cases hidden in different code paths.
**When to Use**
**Triggers:**
- You're testing a function with many input/output combinations
- You're copy-pasting test functions that differ by one or two values
- Adding a new test case requires duplicating 10+ lines of setup/assertion code
**Example — before:**
```go
func TestParseSize(t *testing.T) {
result1, err1 := ParseSize("10MB")
if err1 != nil || result1 != 10_000_000 { t.Error("10MB failed") }
result2, err2 := ParseSize("1GB")
if err2 != nil || result2 != 1_000_000_000 { t.Error("1GB failed") }
result3, err3 := ParseSize("invalid")
if err3 == nil { t.Error("invalid should fail") }
// ... 20 more copy-pasted blocks
}
```
**Example — after:**
```go
func TestParseSize(t *testing.T) {
tests := []struct {
input string
want int64
wantErr bool
}{
{"10MB", 10_000_000, false},
{"1GB", 1_000_000_000, false},
{"invalid", 0, true},
{"0B", 0, false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := ParseSize(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseSize(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
```
### When NOT to Use
**Don't use this when:**
- You have 1–2 test cases with significantly different setup logic — a table adds indirection for no gain
- Each case requires unique assertions or error-checking logic that can't be unified
- The test is inherently sequential (step 2 depends on step 1's output)
**Over-application example:**
```go
func TestMigration(t *testing.T) {
tests := []struct {
name string
// ... 15 fields for setup, teardown, assertions, side effects
}{
{"migrate v1 to v2", /* massive struct literal */},
{"migrate v2 to v3", /* completely different struct literal */},
}
for _, tt := range tests {
// 50 lines of conditional logic because each case is fundamentally different
}
}
```
**Better alternative:**
```go
func TestMigrateV1ToV2(t *testing.T) {
// Clear, self-contained, readable
db := setupV1(t)
err := MigrateToV2(db)
// specific assertions for this migration
}
func TestMigrateV2ToV3(t *testing.T) {
db := setupV2(t)
err := MigrateToV3(db)
// different assertions entirely
}
```
**Why:** Table-driven tests shine when cases share identical setup/assertion logic and differ
only in inputs and expected outputs. When each "case" needs its own control flow, the table
becomes a mini-DSL that's harder to read than separate functions.
**Anti-pattern:** Writing individual assertions for each case, or copy-pasting test functions that differ by one input. **Anti-pattern:** Writing individual assertions for each case, or copy-pasting test functions that differ by one input.
**Code example (stdlib):** **Code example (stdlib):**
@@ -53,7 +147,7 @@ func TestHeaderWrite(t *testing.T) {
### Pattern Name: Named Table Tests with t.Run (Subtests) ### Pattern Name: Named Table Tests with t.Run (Subtests)
**Source:** `/tmp/go-src/src/encoding/json/encode_test.go` lines 285-320, `/tmp/go-src/src/encoding/json/scanner_test.go` lines 30-50 **Source:** [src/encoding/json/encode_test.go#L285](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode_test.go#L285), [src/encoding/json/scanner_test.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/scanner_test.go#L30)
**What they do:** Combine table-driven tests with `t.Run` for named subtests. Use a `CaseName` struct that captures file/line for error reporting. **What they do:** Combine table-driven tests with `t.Run` for named subtests. Use a `CaseName` struct that captures file/line for error reporting.
@@ -88,7 +182,7 @@ func TestValid(t *testing.T) {
### Pattern Name: CaseName with Caller Position Tracking ### Pattern Name: CaseName with Caller Position Tracking
**Source:** `/tmp/go-src/src/encoding/json/internal/jsontest/testcase.go` lines 18-37 **Source:** [src/encoding/json/internal/jsontest/testcase.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/internal/jsontest/testcase.go#L18)
**What they do:** Create a helper type that captures the caller's file:line at the point of test case declaration, so error messages point back to the exact test case definition. **What they do:** Create a helper type that captures the caller's file:line at the point of test case declaration, so error messages point back to the exact test case definition.
@@ -122,7 +216,7 @@ func (pos CasePos) String() string {
### Pattern Name: t.Helper() for Clean Stack Traces ### Pattern Name: t.Helper() for Clean Stack Traces
**Source:** `/tmp/go-src/src/testing/testing.go` lines 1415-1435 **Source:** [src/testing/testing.go#L1415](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing.go#L1415)
**What they do:** Call `t.Helper()` as the first line in any test utility function. This marks the function as a helper, so test failure messages report the caller's line instead of the helper's line. **What they do:** Call `t.Helper()` as the first line in any test utility function. This marks the function as a helper, so test failure messages report the caller's line instead of the helper's line.
@@ -162,7 +256,7 @@ func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
### Pattern Name: *testing.T as First Argument to Helpers ### Pattern Name: *testing.T as First Argument to Helpers
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 4555-4580 **Source:** [src/net/http/serve_test.go#L4555](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L4555)
**What they do:** Pass `*testing.T` (or `testing.TB`) as the first argument to test helper functions, making the dependency on the test context explicit. **What they do:** Pass `*testing.T` (or `testing.TB`) as the first argument to test helper functions, making the dependency on the test context explicit.
@@ -196,7 +290,7 @@ mustGet := func(url string, headers ...string) {
### Pattern Name: t.Cleanup for Test-Scoped Resources ### Pattern Name: t.Cleanup for Test-Scoped Resources
**Source:** `/tmp/go-src/src/testing/testing.go` lines 1439-1468, `/tmp/go-src/src/net/http/clientserver_test.go` lines 120-127 **Source:** [src/testing/testing.go#L1439](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing.go#L1439), [src/net/http/clientserver_test.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L120)
**What they do:** Use `t.Cleanup(fn)` instead of `defer` for resource cleanup in tests. **What they do:** Use `t.Cleanup(fn)` instead of `defer` for resource cleanup in tests.
@@ -256,7 +350,7 @@ ServeFile(w, r, "testdata/file")
### Pattern Name: Golden Files with -update Flag ### Pattern Name: Golden Files with -update Flag
**Source:** `/tmp/go-src/src/cmd/gofmt/gofmt_test.go` lines 18, 113-138 **Source:** [src/cmd/gofmt/gofmt_test.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/cmd/gofmt/gofmt_test.go#L18), 113-138
**What they do:** Compare test output against `.golden` files. Provide a `-update` flag that regenerates golden files from current output when behavior intentionally changes. **What they do:** Compare test output against `.golden` files. Provide a `-update` flag that regenerates golden files from current output when behavior intentionally changes.
@@ -266,6 +360,89 @@ ServeFile(w, r, "testdata/file")
3. Golden files serve as documentation of expected behavior. 3. Golden files serve as documentation of expected behavior.
4. Reviewers can see exactly what output changed in diffs. 4. Reviewers can see exactly what output changed in diffs.
**When to Use**
**Triggers:**
- Your function produces complex multi-line output (formatted code, HTML, JSON, error messages)
- Expected output would be 20+ lines if inlined in the test — unreadable
- Output changes intentionally sometimes and you need a quick way to approve the new version
**Example — before:**
```go
func TestRenderTemplate(t *testing.T) {
got := renderHTML(data)
want := `<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Welcome, Alice</h1>
<p>You have 3 messages.</p>
</body>
</html>` // 8 lines inline — and this is a SIMPLE template
if got != want { t.Errorf("mismatch") }
}
```
**Example — after:**
```go
var update = flag.Bool("update", false, "update golden files")
func TestRenderTemplate(t *testing.T) {
got := renderHTML(data)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, []byte(got), 0644)
return
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Errorf("output mismatch; run with -update to accept new output")
}
}
// Golden file lives at testdata/TestRenderTemplate.golden
```
### When NOT to Use
**Don't use this when:**
- Expected output is short (< 5 lines) — inline it directly for readability
- Output is non-deterministic (timestamps, random IDs, goroutine ordering) without normalization
- The golden file would need updating on every minor refactor — brittle and noisy diffs
**Over-application example:**
```go
// Golden file for a one-line output
var update = flag.Bool("update", false, "update golden files")
func TestVersion(t *testing.T) {
got := Version()
golden := "testdata/TestVersion.golden"
if *update {
os.WriteFile(golden, []byte(got), 0644)
return
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Error("mismatch")
}
}
// testdata/TestVersion.golden contains: "v1.2.3" — seriously?
```
**Better alternative:**
```go
func TestVersion(t *testing.T) {
got := Version()
if got != "v1.2.3" {
t.Errorf("Version() = %q, want %q", got, "v1.2.3")
}
}
```
**Why:** Golden files add process overhead (the `-update` workflow, reviewing diffs in a separate
file). For short, stable outputs, inline comparison is simpler, faster to read, and keeps the
expected value next to the assertion.
**Anti-pattern:** Comparing against inline expected strings that span 50+ lines, or manually constructing expected output. **Anti-pattern:** Comparing against inline expected strings that span 50+ lines, or manually constructing expected output.
**Code example (stdlib):** **Code example (stdlib):**
@@ -313,12 +490,83 @@ func TestRewrite(t *testing.T) {
### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers ### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 387-393 **Source:** [src/net/http/serve_test.go#L387](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L387)
**What they do:** Use `httptest.NewRecorder()` to test HTTP handlers without starting a server. Captures status code, headers, and body. **What they do:** Use `httptest.NewRecorder()` to test HTTP handlers without starting a server. Captures status code, headers, and body.
**Why:** Fast, no network, no port allocation, no goroutines. Perfect for unit testing individual handlers in isolation. **Why:** Fast, no network, no port allocation, no goroutines. Perfect for unit testing individual handlers in isolation.
**When to Use**
**Triggers:**
- You're testing HTTP handler logic (status codes, headers, response body) in isolation
- You don't need real TCP connections, TLS, or routing
- Your test should run in <1ms, not wait for port binding
**Example — before:**
```go
func TestHealthHandler(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(healthHandler))
defer srv.Close()
resp, _ := http.Get(srv.URL + "/health") // real TCP connection — slow
if resp.StatusCode != 200 { t.Fatal("not healthy") }
}
```
**Example — after:**
```go
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req) // direct call — no network
if rec.Code != 200 {
t.Fatalf("got status %d, want 200", rec.Code)
}
if rec.Body.String() != "ok" {
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
}
}
```
### When NOT to Use
**Don't use this when:**
- You need to test real HTTP behavior: TLS handshakes, connection pooling, timeouts, keep-alive
- Your handler depends on server-level middleware (e.g., `http.Server.ConnContext`, TLS client certs)
- You're testing client behavior or redirect-following (need a real URL to connect to)
**Over-application example:**
```go
func TestClientRetries(t *testing.T) {
rec := httptest.NewRecorder()
// Can't test retry logic — there's no real server for the client to connect to!
// rec doesn't have a URL, no TCP, no connection reset simulation
}
```
**Better alternative:**
```go
func TestClientRetries(t *testing.T) {
attempts := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts < 3 {
w.WriteHeader(503)
return
}
w.WriteHeader(200)
}))
defer srv.Close()
// Now test the client's retry behavior against a real server
resp, err := myClient.Get(srv.URL + "/resource")
// ...
}
```
**Why:** `httptest.NewRecorder` tests handler logic in isolation — it has no network, no URL,
no connection lifecycle. When you need to test anything that crosses the network boundary
(clients, retries, TLS, timeouts), you need `httptest.NewServer`.
**Anti-pattern:** Spinning up a full server to test handler logic that doesn't need networking. **Anti-pattern:** Spinning up a full server to test handler logic that doesn't need networking.
**Code example (stdlib):** **Code example (stdlib):**
@@ -345,7 +593,7 @@ func TestServeMuxHandler(t *testing.T) {
### Pattern Name: httptest.NewServer for Integration-Style Tests ### Pattern Name: httptest.NewServer for Integration-Style Tests
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 203-280 **Source:** [src/net/http/clientserver_test.go#L203](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L203)
**What they do:** Use `httptest.NewServer` / `httptest.NewUnstartedServer` for end-to-end HTTP testing with a real TCP listener on localhost. **What they do:** Use `httptest.NewServer` / `httptest.NewUnstartedServer` for end-to-end HTTP testing with a real TCP listener on localhost.
@@ -376,7 +624,7 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c
### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes ### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes
**Source:** `/tmp/go-src/src/encoding/json/bench_test.go` lines 85-101 **Source:** [src/encoding/json/bench_test.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/bench_test.go#L85)
**What they do:** Combine `b.ReportAllocs()` for allocation reporting, `b.RunParallel` for concurrent benchmarks, and `b.SetBytes` for throughput metrics. **What they do:** Combine `b.ReportAllocs()` for allocation reporting, `b.RunParallel` for concurrent benchmarks, and `b.SetBytes` for throughput metrics.
@@ -414,7 +662,7 @@ func BenchmarkCodeEncoder(b *testing.B) {
### Pattern Name: testing.Short() for Expensive Tests ### Pattern Name: testing.Short() for Expensive Tests
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 800, 1000, 2212, 2581 **Source:** [src/net/http/serve_test.go#L800](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L800), 1000, 2212, 2581
**What they do:** Skip slow/flaky/network-dependent tests with `testing.Short()`. The Go CI runs with `-short` in fast mode, full tests in thorough mode. **What they do:** Skip slow/flaky/network-dependent tests with `testing.Short()`. The Go CI runs with `-short` in fast mode, full tests in thorough mode.
@@ -508,7 +756,7 @@ func afterTest(t testing.TB) {
### Pattern Name: Bridge File for Internal Testing ### Pattern Name: Bridge File for Internal Testing
**Source:** `/tmp/go-src/src/net/http/export_test.go` lines 1-50 **Source:** [src/net/http/export_test.go#L1](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/export_test.go#L1)
**What they do:** Create an `export_test.go` file in the package itself (package `http`, not `http_test`) that exports internal symbols to external test packages. Only compiled during testing. **What they do:** Create an `export_test.go` file in the package itself (package `http`, not `http_test`) that exports internal symbols to external test packages. Only compiled during testing.
@@ -533,7 +781,7 @@ var (
### Pattern Name: Generic Test Runner Across Protocol Modes ### Pattern Name: Generic Test Runner Across Protocol Modes
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 100-134 **Source:** [src/net/http/clientserver_test.go#L100](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L100)
**What they do:** A generic `run[T]` function that executes every client/server test in HTTP/1.1, HTTP/2, and HTTP/3 modes automatically. Tests opt into specific modes via options. **What they do:** A generic `run[T]` function that executes every client/server test in HTTP/1.1, HTTP/2, and HTTP/3 modes automatically. Tests opt into specific modes via options.
@@ -564,7 +812,7 @@ func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
### Pattern Name: io.Writer Adapter for *testing.T ### Pattern Name: io.Writer Adapter for *testing.T
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 337-345 **Source:** [src/net/http/clientserver_test.go#L337](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L337)
**What they do:** Implement `io.Writer` backed by `t.Logf`, so server error logs appear in test output (visible with `-v`, suppressed otherwise). **What they do:** Implement `io.Writer` backed by `t.Logf`, so server error logs appear in test output (visible with `-v`, suppressed otherwise).
@@ -584,3 +832,5 @@ func (w testLogWriter) Write(b []byte) (int, error) {
// Usage: // Usage:
cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0) cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0)
``` ```
<!-- PATTERN_COMPLETE -->
+413 -202
View File
@@ -1,262 +1,473 @@
# Anti-Patterns: What Kubernetes Avoids (and Why) # Anti-Patterns: What Go's Stdlib Avoids (and Why)
## 1. Never Mutate Shared Cache Objects Patterns the Go standard library team actively avoids, extracted
from studying what they DON'T do in their source code.
**What they avoid:** Modifying objects returned by Listers/Informers without deep-copying first. **Source:** [golang/go](https://github.com/golang/go) at commit
[`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
**Why:** The informer cache is shared across all controllers. Mutating a cached object corrupts state for every other consumer.
**The pattern K8s enforces:**
```go
// WRONG — mutates shared cache
deployment, _ := dc.dLister.Deployments(ns).Get(name)
deployment.Spec.Replicas = ptr.To[int32](3) // CORRUPTION!
// RIGHT — deep copy before mutating
deployment, _ := dc.dLister.Deployments(ns).Get(name)
deploymentCopy := deployment.DeepCopy()
deploymentCopy.Spec.Replicas = ptr.To[int32](3)
```
**Evidence:** The `runtime.Object` interface *mandates* `DeepCopyObject()`. Every API type has generated deep copy methods. The entire architecture assumes immutable reads.
--- ---
## 2. Never Process the Same Key Concurrently ## 1. Returning Errors and Values Simultaneously
**What they avoid:** Multiple goroutines syncing the same object simultaneously. **What they avoid:** Functions that return a valid value alongside
a non-nil error.
**Why:** Two goroutines reading the same Deployment, each computing a different desired state, then both writing → conflict errors and potential state corruption. **Source evidence:** The entire stdlib follows: if error is non-nil,
other return values are undefined/zero. `io.Reader.Read` is the ONE
exception (documented as "n > 0 AND err possible").
**The pattern K8s enforces:** The workqueue's `processing` set ensures a key is only handed to one worker at a time. If an item is added while being processed, it's re-queued *after* `Done()` is called: **Why it's bad:** Callers check the error OR use the value — not both.
If you return data with an error, callers might ignore the error
because they got "valid" data.
```go ```go
// From queue.go — the processing set blocks concurrent access // BAD — ambiguous: is result valid when err != nil?
func (q *Typed[T]) Get() (item T, shutdown bool) { func fetch(url string) ([]byte, error) {
// ... resp, err := http.Get(url)
q.processing.Insert(item) // Mark as being worked on if err != nil {
q.dirty.Delete(item) return partialData, err // caller might use partialData without checking err
return item, false
}
```
---
## 3. Never Use Edge-Triggered Logic
**What they avoid:** Controllers that react to *what changed* rather than *what the current state is*.
**Why:** Events can be missed (watch disconnects), delivered out of order, or duplicated. If your controller says "a pod was deleted, so decrement counter" rather than "count current pods and compare to desired", you'll drift.
**The pattern K8s enforces:** Level-triggered reconciliation. The `syncHandler` reads *current state from the cache*, computes *desired state from the spec*, and makes the world match:
```go
// The sync function always reads current state, never relies on "what happened"
func (dc *DeploymentController) syncDeployment(ctx context.Context, key string) error {
deployment, err := dc.dLister.Deployments(namespace).Get(name)
// Compute desired state from deployment.Spec
// Read actual state from replicaset lister
// Reconcile difference
}
```
---
## 4. Never Forget to Call queue.Forget() on Success
**What they avoid:** Letting the rate limiter track items that succeeded.
**Why:** The rate limiter is per-item. If you never call `Forget()`, the next time the same key needs processing (even for a new event), it will be rate-limited as if it previously failed.
**Source:** The rate_limiting_queue.go comments explicitly warn:
```go
// NewTypedRateLimitingQueue constructs a new workqueue with rateLimited queuing ability
// Remember to call Forget! If you don't, you may end up tracking failures forever.
```
**The pattern K8s enforces:**
```go
func (dc *DeploymentController) handleErr(ctx context.Context, err error, key string) {
if err == nil {
dc.queue.Forget(key) // CRITICAL: clear the failure counter
return
} }
if dc.queue.NumRequeues(key) < maxRetries { return io.ReadAll(resp.Body)
dc.queue.AddRateLimited(key) }
return
// GOOD — nil data when error is non-nil
func fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
} }
dc.queue.Forget(key) // Also forget when giving up return io.ReadAll(resp.Body)
} }
``` ```
--- ### When to Apply This Rule
## 5. Never Hit the API Server in a Tight Loop **Triggers:**
- Returning non-zero values alongside non-nil errors
- Callers that use the value without checking the error
**What they avoid:** Direct API calls for reads. List/Get calls in hot paths. ### Exceptions
**Why:** The API server is a shared resource. If 100 controllers each make 10 API calls per reconciliation at 1 sync/second, that's 1000 req/s to the API server per controller manager instance. - `io.Reader.Read()` — explicitly documented as "n > 0 AND err == io.EOF" case
- Functions that return "best effort" partial results (document this clearly)
**The pattern K8s enforces:** Read from Listers (local cache), write to API server:
```go
// READ from cache (free, local, fast)
deployment, err := dc.dLister.Deployments(namespace).Get(name)
// WRITE to API server (expensive, remote, rate-limited)
_, err = dc.client.AppsV1().Deployments(namespace).Update(ctx, deployment, ...)
```
--- ---
## 6. Never Sync Before Caches Are Warm ## 2. Large Interfaces (Java-Style)
**What they avoid:** Processing items before the informer has done its initial List. **What they avoid:** Interfaces with more than 1-3 methods.
**Why:** With an empty cache, a controller might think "no pods exist, must create all of them" — causing a thundering herd of duplicate creates. **Source evidence:** `io.Reader` (1 method), `io.Writer` (1 method),
`fmt.Stringer` (1 method), `sort.Interface` (3 methods). The largest
stdlib interface is `net.Conn` at 8 methods — and it's widely
considered too large.
**Why it's bad:** Large interfaces are hard to implement (fewer types
satisfy the contract). Small interfaces compose: `io.ReadWriter` is
just `Reader + Writer`. The bigger the interface, the tighter the
coupling.
**The pattern K8s enforces:**
```go ```go
// pkg/controller/deployment/deployment_controller.go:189 // BAD — Java-style "service" interface
if !cache.WaitForNamedCacheSyncWithContext(ctx, type UserService interface {
dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) { Create(ctx context.Context, u *User) error
return // Don't start workers until all caches are populated Get(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, filter Filter) ([]*User, error)
Count(ctx context.Context) (int, error)
Search(ctx context.Context, q string) ([]*User, error)
} }
// Only ONE implementation will ever satisfy this. It's a class in disguise.
// GOOD — small, composable interfaces
type UserReader interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
SaveUser(ctx context.Context, u *User) error
}
// Functions accept the smallest interface they need
func sendWelcome(ctx context.Context, r UserReader, id string) error { ... }
``` ```
### When to Apply This Rule
**Triggers:**
- Interface with 4+ methods
- Only one implementation exists
- Interface defined in the same package as the implementation
### Exceptions
- Interfaces matching an external protocol (net.Conn — mirrors TCP API)
- Well-known stdlib patterns (sort.Interface — exactly 3 methods)
--- ---
## 7. Never Ignore Tombstones in Delete Handlers ## 3. Package-Level init() for Complex Logic
**What they avoid:** Assuming delete handlers always receive the concrete type. **What they avoid:** Using `init()` for anything beyond simple
registration or flag setup.
**Why:** If a watch disconnects and reconnects, missed deletes arrive as `DeletedFinalStateUnknown` (tombstones). Ignoring them means your controller never learns about those deletions. **Source evidence:** Stdlib init() functions are exclusively used for:
registering encodings, initializing hash algorithms, setting up flags.
Never for: opening files, making network calls, or complex computation.
**Why it's bad:** init() runs at import time — you can't control when.
Can't pass parameters. Can't return errors. Can't skip in tests.
Hard to debug (runs before main). Creates import side effects.
**The pattern K8s enforces:**
```go ```go
// Every delete handler must check for tombstones // BAD — complex logic in init
func (dc *DeploymentController) deleteDeployment(logger klog.Logger, obj interface{}) { func init() {
d, ok := obj.(*apps.Deployment) db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if !ok { if err != nil {
tombstone, ok := obj.(cache.DeletedFinalStateUnknown) log.Fatal(err) // crashes the program at import time
if !ok {
utilruntime.HandleError(...)
return
} }
d, ok = tombstone.Obj.(*apps.Deployment) globalDB = db
// ... }
// GOOD — explicit initialization
func NewApp(dsn string) (*App, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
} }
return &App{db: db}, nil
} }
``` ```
### When to Apply This Rule
**Triggers:**
- init() that opens files, network connections, or databases
- init() with error handling (log.Fatal/panic)
- init() that takes >5 lines
- init() that depends on environment variables
### Exceptions
- Registering encodings: `encoding/gob`, `image/png` init registration
- Seeding randomness or initializing lookup tables
- Flag registration (`flag.String(...)` in init)
--- ---
## 8. Never Use ResourceVersion for Equality ## 4. Stuttering Names
**What they avoid:** Comparing ResourceVersion to check if an object changed. **What they avoid:** Package-qualified names that repeat the package
name: `http.HTTPServer`, `user.UserService`, `json.JSONEncoder`.
**Why:** ResourceVersion is opaque (currently etcd's mod_revision, but this is an implementation detail). The only valid operation is "!=" to detect change. **Source evidence:** The stdlib uses: `http.Server` (not HTTPServer),
`json.Encoder` (not JSONEncoder), `bytes.Buffer` (not BytesBuffer).
The package name provides context.
**Why it's bad:** When you import and use `http.HTTPServer`, you say
"HTTP" twice. Go's package system already provides namespace context.
Stuttering is visual noise that adds no information.
**The pattern K8s uses:**
```go ```go
// pkg/controller/deployment/deployment_controller.go:284-288 // BAD — stuttering
func (dc *DeploymentController) updateReplicaSet(logger klog.Logger, old, cur interface{}) { package user
curRS := cur.(*apps.ReplicaSet)
oldRS := old.(*apps.ReplicaSet) type UserService struct { ... } // user.UserService
if curRS.ResourceVersion == oldRS.ResourceVersion { type UserRepository interface { ... } // user.UserRepository
return // Periodic resync, nothing actually changed func NewUserService() *UserService { ... }
// GOOD — the package name provides context
package user
type Service struct { ... } // user.Service
type Repository interface { ... } // user.Repository
func NewService() *Service { ... }
```
### When to Apply This Rule
**Triggers:**
- Type name starts with the package name
- `New<PackageName><Type>` constructor pattern
- Reading the qualified name aloud sounds redundant
### Exceptions
- When omitting the prefix would be ambiguous (`error.Error` — Go uses this)
- Single-package binaries where qualification doesn't apply
---
## 5. Naked Returns in Long Functions
**What they avoid:** Using named return values with bare `return`
in functions longer than a few lines.
**Source evidence:** The stdlib uses naked returns only in very short
functions (1-5 lines). Longer functions always return explicit values.
The `go vet` tool warns about this.
**Why it's bad:** In a 50-line function with naked return, you have
to trace every assignment to the named returns to understand what's
being returned. It's like having invisible variables.
```go
// BAD — naked return in long function
func process(data []byte) (result int, err error) {
// ... 40 lines of code ...
if something {
result = compute()
return // what is err here? was it set earlier? who knows
} }
// ... process the real update // ... more code ...
} return // mystery values
```
---
## 9. Never Panic in Production Goroutines (Without Recovery)
**What they avoid:** Unhandled panics killing the entire controller manager.
**Why:** A single nil pointer in one controller's sync loop would crash all 30+ controllers running in the same process.
**The pattern K8s enforces:**
```go
// Every goroutine gets crash protection
func (dc *DeploymentController) Run(ctx context.Context, workers int) {
defer utilruntime.HandleCrash() // Top-level recovery
// ...
} }
// And in every polling loop: // GOOD — explicit returns
func BackoffUntilWithContext(ctx context.Context, f func(ctx context.Context), ...) { func process(data []byte) (int, error) {
func() { // ... 40 lines of code ...
defer runtime.HandleCrashWithContext(ctx) // Per-iteration recovery if something {
f(ctx) return compute(), nil // clear what's being returned
}()
}
```
---
## 10. Never Block Workers Indefinitely
**What they avoid:** Unbounded blocking in a sync handler (e.g., waiting for a condition that may never occur).
**Why:** Workers are a finite pool. If one blocks forever, that's one fewer worker processing the queue. At scale, this cascades.
**The pattern K8s enforces:** All API calls take context (with timeouts), all waits are bounded:
```go
// Timeouts on API calls
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
_, err := client.CoreV1().Pods(ns).Create(ctx, pod, metav1.CreateOptions{})
```
---
## 11. Never Use sync.Mutex Where sync.Once Suffices
**What they avoid:** Full mutual exclusion for one-shot operations.
**Why:** `sync.Once` is semantically clearer and avoids the bug where you forget to check a "done" flag under the mutex.
**The pattern K8s uses:**
```go
// pkg/controller/controller_ref_manager.go:43-49
type BaseControllerRefManager struct {
canAdoptErr error
canAdoptOnce sync.Once // One-shot lazy evaluation
CanAdoptFunc func(ctx context.Context) error
}
func (m *BaseControllerRefManager) CanAdopt(ctx context.Context) error {
m.canAdoptOnce.Do(func() {
if m.CanAdoptFunc != nil {
m.canAdoptErr = m.CanAdoptFunc(ctx)
} }
}) return 0, fmt.Errorf("processing: %w", err)
return m.canAdoptErr
} }
``` ```
--- ### When to Apply This Rule
## 12. Never Expose Mutable State Through Interfaces **Triggers:**
- Function body > 10 lines with naked returns
- Multiple return points with naked returns
- Named returns used ONLY for naked return (not for documentation)
**What they avoid:** Returning pointers to internal state through public interfaces. ### Exceptions
**Why:** Callers can accidentally mutate internal state, creating subtle bugs that only manifest under concurrency. - Short functions (3-5 lines) where named returns add clarity
- Defer-based error wrapping: `defer func() { err = wrap(err) }()`
**The pattern K8s enforces:** Listers return objects from the read-only cache. The `DeepCopy()` pattern ensures mutation safety is the caller's responsibility, not the cache's. (this is the primary legitimate use of named returns)
--- ---
## Summary: The Philosophy ## 6. Error String Formatting (Capitalization/Punctuation)
Kubernetes avoids these anti-patterns because of one fundamental truth: **in a distributed system, every assumption you make about state being consistent is wrong.** **What they avoid:** Error messages that start with capitals or end
with punctuation.
The patterns exist because: **Source evidence:** Every error string in the stdlib is lowercase,
1. **Events are unreliable** → level-triggered reconciliation no trailing period. `fmt.Errorf("open %s: %w", path, err)` — never
2. **Reads are stale** → always compare desired vs actual `"Failed to open file."`.
3. **Concurrent access is inevitable** → deep copy, queue serialization
4. **Failures are normal** → retry with backoff, graceful degradation **Why it's bad:** Errors compose. `fmt.Errorf("connect: %w", err)`
5. **Resources are shared** → cache reads, rate-limit writes produces `connect: open /etc/hosts: permission denied`. If inner
6. **Systems outlive their authors** → code generation, type registries, feature gates errors are capitalized or punctuated, the chain looks broken:
`connect: Failed to open file.`
```go
// BAD — capitalized, punctuated
return fmt.Errorf("Failed to open configuration file: %s.", path)
// GOOD — lowercase, no punctuation, wraps cleanly
return fmt.Errorf("open config %s: %w", path, err)
```
### When to Apply This Rule
**Triggers:**
- Error strings starting with uppercase
- Error strings ending with `.` or `!`
- Error messages that don't compose well with wrapping
### Exceptions
- Proper nouns: `"Google API returned 503"`
- Acronyms at the start: `"DNS lookup failed"` (some teams accept this)
---
## 7. Overusing Channels (When Mutex Suffices)
**What they avoid:** Using channels for simple mutual exclusion
where `sync.Mutex` would be clearer.
**Source evidence:** The stdlib uses Mutex 540 times vs channels
for synchronization ~50 times. Channels are for communication
between goroutines. Mutex is for protecting shared state.
**Why it's bad:** A channel of capacity 1 as a mutex is clever
but obscure. It adds cognitive load, makes locking/unlocking
asymmetric, and doesn't have `defer mu.Unlock()` ergonomics.
```go
// BAD — channel as mutex (clever but obscure)
sem := make(chan struct{}, 1)
sem <- struct{}{} // "lock"
// ... critical section ...
<-sem // "unlock"
// GOOD — mutex is clear and idiomatic
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
// ... critical section ...
```
### When to Apply This Rule
**Triggers:**
- `make(chan struct{}, 1)` used as a lock
- Channel send/receive pairs that don't actually communicate data
- Complex select statements where a simple lock would work
### Exceptions
- Rate limiting (semaphore with capacity > 1)
- Signaling between goroutines (cancellation, done channels)
- Fan-out/fan-in patterns (actual communication)
---
## 8. Accepting Interfaces, Returning Interfaces
**What they avoid:** Returning interfaces from functions (except
well-known ones like `error`).
**Source evidence:** The Go proverb: "Accept interfaces, return structs."
The stdlib returns concrete types: `http.NewServeMux()` returns
`*ServeMux`, not `http.Handler`. This allows callers to access
all methods.
**Why it's bad:** Returning an interface hides the concrete type.
Callers can't access methods that aren't in the interface. Makes
testing harder (can't construct the concrete type). Prevents
future method additions without breaking the interface.
```go
// BAD — returns interface (hides concrete type)
func NewCache() Cacher {
return &redisCache{...}
}
// Callers can't access redis-specific methods or fields
// GOOD — returns concrete type
func NewCache() *RedisCache {
return &RedisCache{...}
}
// Callers get the full type. They can still use it as Cacher where needed.
```
### When to Apply This Rule
**Triggers:**
- Factory functions returning interfaces
- Single-implementation interfaces returned from constructors
- Interface return types that prevent callers from accessing useful methods
### Exceptions
- Standard error returns (`error` interface)
- When there genuinely are multiple implementations chosen at runtime
- Plugin systems where the concrete type must be hidden
---
## 9. Panic in Library Code
**What they avoid:** Using `panic()` for conditions callers could
handle.
**Source evidence:** stdlib panic usage is exclusively for: (1) index
out of bounds, (2) nil pointer on documented non-nil parameter,
(3) impossible states (unreachable code). Never for "file not found"
or "invalid input" style errors.
**Why it's bad:** panic kills the goroutine (and the program unless
recovered). Library code shouldn't decide that an error is fatal —
that's the caller's decision.
```go
// BAD — panicking on recoverable error
func MustParse(s string) Config {
cfg, err := Parse(s)
if err != nil {
panic(err) // caller has no way to handle bad input
}
return cfg
}
// GOOD — return error, let caller decide
func Parse(s string) (Config, error) {
// ... parse logic ...
if invalid {
return Config{}, fmt.Errorf("parse config: %w", err)
}
return cfg, nil
}
```
### When to Apply This Rule
**Triggers:**
- `panic()` in exported functions on user input
- `Must*` functions without a non-panicking alternative
- `log.Fatal` in library code (same as panic — kills the process)
### Exceptions
- `Must*` pattern for compile-time constants: `regexp.MustCompile("^\\d+$")`
- Truly impossible states after exhaustive validation
- Test helpers that panic on setup failure
---
## 10. Unexported Interface Satisfaction
**What they avoid:** Defining interfaces in the implementor's package
rather than the consumer's package.
**Source evidence:** The Go wiki explicitly says: "Define interfaces
in the package that USES them, not the package that implements them."
The stdlib follows this — `io.Reader` is in `io` (consumer), not
in `os` or `net` (implementors).
**Why it's bad:** Pre-declaring interfaces in the implementor package
couples consumer to producer. It's Java-style "implements Readable"
thinking. In Go, satisfaction is implicit — the consumer defines
what it needs.
```go
// BAD — interface in the implementation package
package storage
type Store interface { // defined where it's implemented
Get(key string) ([]byte, error)
Set(key string, val []byte) error
}
type RedisStore struct { ... }
// Now everyone who imports storage gets this interface forced on them
// GOOD — interface in the consumer package
package handler
type Getter interface { // defined where it's used
Get(key string) ([]byte, error)
}
func NewHandler(store Getter) *Handler { ... }
// handler only depends on what it actually needs
```
### When to Apply This Rule
**Triggers:**
- Interface defined next to its only implementation
- Interface in the same package as the struct that satisfies it
- Interface with methods matching exactly one struct
### Exceptions
- Well-known "standard" interfaces (io.Reader, sort.Interface)
- Interfaces that define a protocol many packages implement
- Plugin/driver interfaces where the contract IS the package's purpose
<!-- PATTERN_COMPLETE -->
+646
View File
@@ -0,0 +1,646 @@
# Common Mistakes in Go
Code smells that come from writing Go like it's Java, Python, or C++.
These are patterns that compile fine but cause real problems at runtime,
in reviews, or when the codebase grows.
Each entry shows what people do, why it hurts, and what idiomatic Go looks like instead.
---
## 1. Nil Pointer Check After Use
**What they avoid:** Checking whether a pointer is nil before dereferencing it.
**Why it's bad:** The program panics on the dereference line, not at the nil check.
The nil check below the access is dead code — the crash already happened. This
pattern is especially insidious because it *looks* like the author considered nil
safety, and reviewers gloss over it.
```go
// BAD
func process(cfg *Config) {
name := cfg.Name // panics here if cfg is nil
if cfg == nil {
return
}
fmt.Println(name)
}
```
```go
// GOOD
func process(cfg *Config) {
if cfg == nil {
return
}
name := cfg.Name
fmt.Println(name)
}
```
### When to Apply This Rule
- Any function that receives a pointer parameter
- Methods on types that could be called on a nil receiver
- After type assertions that return a pointer (`val, ok := x.(*Foo)`)
### Exceptions
- Methods with documented nil-receiver behavior (e.g., `(*bytes.Buffer).Len()` returns 0 on nil)
- When the contract explicitly guarantees non-nil (internal unexported helpers where callers are controlled)
---
## 2. Goroutine Leak
**What they avoid:** Ensuring every goroutine has a path to termination.
**Why it's bad:** Leaked goroutines hold memory, file descriptors, and network
connections forever. In long-running services, this is a slow-motion OOM. The
goroutine count climbs monotonically and the only fix is a restart.
```go
// BAD
func fetchAll(urls []string) []string {
results := make(chan string)
for _, url := range urls {
go func(u string) {
resp, err := http.Get(u)
if err != nil {
return // goroutine exits, but nobody reads from channel
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
results <- string(body) // blocks forever if caller stops reading
}(url)
}
var out []string
// Only read first 3 — remaining goroutines leak
for i := 0; i < 3; i++ {
out = append(out, <-results)
}
return out
}
```
```go
// GOOD
func fetchAll(ctx context.Context, urls []string) ([]string, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]string, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
results[i] = string(body)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
```
### When to Apply This Rule
- Every `go func()` call — ask "what makes this goroutine stop?"
- Any channel send without a guaranteed receiver
- Background workers in servers (they need shutdown signals)
### Exceptions
- Goroutines that are genuinely meant to run for the process lifetime (e.g., a single background GC loop with no resources to release)
- Fire-and-forget logging where the channel is buffered and drained at shutdown
---
## 3. Interface Pollution
**What they avoid:** Keeping interfaces small and focused.
**Why it's bad:** Large interfaces (Java-style `Service` with 10+ methods) are
impossible to mock, impossible to satisfy partially, and couple every consumer
to every method. They defeat Go's implicit interface satisfaction — the primary
mechanism for decoupling.
```go
// BAD — Java-style "service interface"
type UserService interface {
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
Get(ctx context.Context, id string) (*User, error)
List(ctx context.Context, filter Filter) ([]*User, error)
Search(ctx context.Context, q string) ([]*User, error)
Activate(ctx context.Context, id string) error
Deactivate(ctx context.Context, id string) error
ChangePassword(ctx context.Context, id, pw string) error
ResetPassword(ctx context.Context, id string) error
AssignRole(ctx context.Context, id, role string) error
RemoveRole(ctx context.Context, id, role string) error
}
```
```go
// GOOD — small interfaces defined by consumers
type UserGetter interface {
Get(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
Create(ctx context.Context, u *User) error
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
}
// Handlers accept only what they need
func NewProfileHandler(users UserGetter) *ProfileHandler {
return &ProfileHandler{users: users}
}
```
### When to Apply This Rule
- When defining an interface with more than 3-5 methods
- When test mocks are painful to write
- When multiple consumers only use a subset of methods
### Exceptions
- Standard library compatibility (e.g., implementing `http.Handler`, `io.ReadWriteCloser`)
- Internal package boundaries where the full surface is always consumed together
- Wrapper types that must delegate the entire surface (e.g., middleware around a DB driver)
---
## 4. Stuttering Names
**What they avoid:** Considering how a name reads at the call site with its package qualifier.
**Why it's bad:** Go names are always read with their package prefix: `http.HTTPClient`
becomes "HTTP HTTP Client." This is noise. The stdlib gets this right (`http.Client`,
not `http.HTTPClient`), but newcomers from Java/C# (where package names aren't part
of the identifier at the call site) repeat context constantly.
```go
// BAD
package user
type UserService struct{} // user.UserService
type UserRepository struct{} // user.UserRepository
func NewUserService() {} // user.NewUserService
package http
type HTTPClient struct{} // http.HTTPClient
type HTTPResponse struct{} // http.HTTPResponse
```
```go
// GOOD
package user
type Service struct{} // user.Service
type Repository struct{} // user.Repository
func NewService() {} // user.NewService
package http
type Client struct{} // http.Client
type Response struct{} // http.Response
```
### When to Apply This Rule
- Exported types, functions, and constants — read them aloud with the package prefix
- If you hear the same word twice, you have stutter
### Exceptions
- When removing the prefix creates genuine ambiguity (`log.Logger` is fine even though `log.Log` might be "simpler")
- Test helper packages where the package name is generic (e.g., `testutil.TestHelper` — but consider renaming the package instead)
---
## 5. init() Abuse
**What they avoid:** Making initialization explicit and testable.
**Why it's bad:** `init()` runs at import time with no arguments and no error
return. You can't test it in isolation, can't skip it, can't control ordering
across packages, and can't handle errors gracefully. Complex init() functions
create invisible dependencies and make binaries fail at startup with no clear
trace.
```go
// BAD
package database
var db *sql.DB
func init() {
var err error
db, err = sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err) // crashes at import time
}
if err := db.Ping(); err != nil {
log.Fatal(err) // network call during init
}
}
func GetDB() *sql.DB { return db }
```
```go
// GOOD
package database
type DB struct {
conn *sql.DB
}
func Open(dsn string) (*DB, error) {
conn, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("database open: %w", err)
}
if err := conn.Ping(); err != nil {
conn.Close()
return nil, fmt.Errorf("database ping: %w", err)
}
return &DB{conn: conn}, nil
}
```
### When to Apply This Rule
- init() that does I/O (network, disk, environment reads)
- init() that can fail (anything with an error you're ignoring or fatal-ing on)
- init() with side effects that affect tests
### Exceptions
- Registering drivers/codecs (`sql.Register`, `image.RegisterFormat`) — the standard pattern
- Setting simple package-level defaults that don't involve I/O
- `init()` that registers flags (before `flag.Parse()` runs)
---
## 6. Ignoring Errors
**What they avoid:** Handling (or explicitly documenting) every error return.
**Why it's bad:** `_ = someFunc()` silently swallows failures. The code continues
with invalid state, corrupt data, or half-finished operations. When things break
later, the root cause is invisible — you're debugging symptoms three layers removed
from the actual failure.
```go
// BAD
func saveUser(u *User) {
data, _ := json.Marshal(u) // what if Marshal fails?
_ = os.WriteFile("user.json", data, 0644) // what if disk is full?
_ = notifyService(u.ID) // what if service is down?
}
```
```go
// GOOD
func saveUser(u *User) error {
data, err := json.Marshal(u)
if err != nil {
return fmt.Errorf("marshal user %s: %w", u.ID, err)
}
if err := os.WriteFile("user.json", data, 0644); err != nil {
return fmt.Errorf("write user file: %w", err)
}
if err := notifyService(u.ID); err != nil {
// Log but don't fail — notification is best-effort
log.Printf("warn: notify failed for user %s: %v", u.ID, err)
}
return nil
}
```
### When to Apply This Rule
- Every function call that returns an error
- Deferred calls that can fail (`defer f.Close()` — consider checking the error)
- Type assertions without the comma-ok form
### Exceptions
- `fmt.Fprintf` to stdout/stderr in CLI tools (the program is about to exit anyway)
- `(*bytes.Buffer).Write` — documented to never return an error
- Hash `Write` methods (`hash.Hash` — documented to never error)
---
## 7. Returning Concrete Types from Constructors
**What they avoid:** Returning interfaces from constructors to enable decoupling.
**Why it's bad:** Wait — this one is backwards from what Java/C# developers expect.
In Go, **returning concrete types from constructors is correct**. The smell is
the *opposite*: returning an interface from a constructor, which hides the concrete
type unnecessarily, prevents access to type-specific methods, and makes the code
harder to understand.
The Go proverb: "Accept interfaces, return structs."
```go
// BAD — returning interface from constructor (Java factory pattern)
type Storage interface {
Save(key string, data []byte) error
Load(key string) ([]byte, error)
}
// Caller can't access DiskStorage-specific methods (like Sync)
func NewStorage(path string) Storage {
return &DiskStorage{path: path}
}
```
```go
// GOOD — return concrete, accept interface where needed
type DiskStorage struct {
path string
}
func NewDiskStorage(path string) *DiskStorage {
return &DiskStorage{path: path}
}
func (d *DiskStorage) Save(key string, data []byte) error { /* ... */ }
func (d *DiskStorage) Load(key string) ([]byte, error) { /* ... */ }
func (d *DiskStorage) Sync() error { /* ... */ }
// Consumers declare what they need
type Saver interface {
Save(key string, data []byte) error
}
func NewUploader(s Saver) *Uploader {
return &Uploader{store: s}
}
```
### When to Apply This Rule
- Constructor functions (`New...`) — return the concrete pointer type
- Interfaces should be defined by consumers, not producers
- When you catch yourself writing a factory that returns an interface
### Exceptions
- When the constructor genuinely chooses between multiple implementations based on config (factory pattern with a legitimate reason)
- Standard library patterns like `errors.New` returning `error` (the interface IS the contract)
---
## 8. sync.Mutex Value Copying
**What they avoid:** Ensuring mutexes are never copied.
**Why it's bad:** A `sync.Mutex` contains internal state. Copying a locked mutex
creates a second mutex that is *also* locked — but independently. The copy and
original now protect nothing. This leads to data races that are invisible to the
race detector in some cases, because both copies appear to lock/unlock correctly
in isolation.
```go
// BAD — mutex embedded in struct passed by value
type Counter struct {
mu sync.Mutex
count int
}
func (c Counter) Value() int { // VALUE RECEIVER — copies the mutex!
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func printCounter(c Counter) { // passed by value — copies the mutex!
fmt.Println(c.Value())
}
```
```go
// GOOD
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Value() int { // pointer receiver
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func printCounter(c *Counter) { // passed by pointer
fmt.Println(c.Value())
}
```
### When to Apply This Rule
- Any struct containing `sync.Mutex`, `sync.RWMutex`, `sync.WaitGroup`, or `sync.Cond`
- All methods on such structs must use pointer receivers
- Never pass such structs by value to functions
- Run `go vet` — it catches some (but not all) mutex copy violations
### Exceptions
- None. There is no valid reason to copy a mutex. If `go vet` flags it, fix it.
---
## 9. Channel Misuse for Simple Synchronization
**What they avoid:** Using the simplest synchronization primitive for the job.
**Why it's bad:** Channels are for communication. When you just need "wait for N
goroutines" or "protect a shared variable," channels add unnecessary complexity:
extra goroutines to drain them, easy-to-miss deadlocks, and cognitive overhead
for readers. Using a channel where a `sync.WaitGroup` or `sync.Mutex` would suffice
is like using a message queue to increment a counter.
```go
// BAD — channel as a WaitGroup
func processAll(items []Item) {
done := make(chan struct{})
for _, item := range items {
go func(it Item) {
process(it)
done <- struct{}{}
}(item)
}
// Wait for all
for range items {
<-done
}
}
// BAD — channel as a Mutex
func safeIncrement(counter *int, ch chan struct{}) {
ch <- struct{}{} // "lock"
*counter++
<-ch // "unlock"
}
```
```go
// GOOD — WaitGroup for fan-out-wait
func processAll(items []Item) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(it Item) {
defer wg.Done()
process(it)
}(item)
}
wg.Wait()
}
// GOOD — Mutex for shared state
type SafeCounter struct {
mu sync.Mutex
n int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
```
### When to Apply This Rule
- "Wait for N things to finish" → `sync.WaitGroup`
- "Protect shared state" → `sync.Mutex` / `sync.RWMutex`
- "Limit concurrency" → semaphore (`chan struct{}` with buffer) or `golang.org/x/sync/semaphore`
### Exceptions
- When you genuinely need to stream results back (producer/consumer) — channels are correct
- Select with cancellation — channels compose with `context.Done()`
- Pipeline patterns where data flows between stages
---
## 10. Premature Abstraction
**What they avoid:** Waiting until a pattern emerges before creating an abstraction.
**Why it's bad:** Defining an interface before you have two implementations means
you're guessing at the contract. The interface will be wrong — too broad, too narrow,
or shaped around implementation details of the single concrete type. Now you're
locked into a bad abstraction that's harder to change than no abstraction at all.
The Go proverb: "A little copying is better than a little dependency."
```go
// BAD — interface before the need exists
type Cache interface {
Get(key string) ([]byte, error)
Set(key string, val []byte, ttl time.Duration) error
Delete(key string) error
Keys(pattern string) ([]string, error)
Flush() error
}
// Only implementation
type RedisCache struct{ client *redis.Client }
// The interface was designed around Redis's capabilities.
// When you add Memcached, half the methods don't map cleanly.
```
```go
// GOOD — start concrete, extract when needed
type RedisCache struct {
client *redis.Client
}
func (r *RedisCache) Get(key string) ([]byte, error) { /* ... */ }
func (r *RedisCache) Set(key string, val []byte, ttl time.Duration) error { /* ... */ }
func (r *RedisCache) Delete(key string) error { /* ... */ }
func (r *RedisCache) Keys(pattern string) ([]string, error) { /* ... */ }
func (r *RedisCache) Flush() error { /* ... */ }
// Later, when you ACTUALLY add a second implementation:
// Extract only the interface that consumers need (see #3)
type Getter interface {
Get(key string) ([]byte, error)
}
// The handler only needs Get — discovered through real usage
func NewHandler(cache Getter) *Handler {
return &Handler{cache: cache}
}
```
### When to Apply This Rule
- You're about to write an interface and there's only one implementation
- You're adding an interface "for testing" — consider whether a thin concrete wrapper is simpler
- You're designing a "framework" inside an application
### Exceptions
- Interfaces required by external contracts (stdlib signatures, plugin systems)
- When the interface genuinely represents a boundary you know will have multiple implementations (e.g., `io.Reader` — the abstraction predates and outlives any single implementation)
- Test doubles for external services (database, HTTP APIs) where you control the consumer interface
---
## Summary
| # | Smell | Root Cause |
|---|-------|-----------|
| 1 | Nil check after use | Mechanical nil checks without thinking about order |
| 2 | Goroutine leak | "Fire and forget" mentality from async/await languages |
| 3 | Interface pollution | Java's "program to an interface" taken literally |
| 4 | Stuttering names | Ignoring that package name is part of the identifier |
| 5 | init() abuse | Treating init() like a class constructor |
| 6 | Ignoring errors | Python's "easier to ask forgiveness" without the try/except |
| 7 | Interface-returning constructors | Java/C# factory pattern cargo-culting |
| 8 | Mutex value copying | Not understanding Go's value semantics |
| 9 | Channel misuse | "Channels are Go's thing so I should use them everywhere" |
| 10 | Premature abstraction | OOP instinct to abstract before understanding |
---
*These patterns are drawn from real code review feedback, the Go stdlib design,
and common issues flagged by `go vet`, `staticcheck`, and `golangci-lint`.*
<!-- PATTERN_COMPLETE -->
+15
View File
@@ -0,0 +1,15 @@
# Sources
Reference material extracted from specific projects. Study for ideas, don't copy blindly.
These are **descriptive** — they document what a project does and why.
The `patterns/` directory is **prescriptive** — it tells you what to do.
Patterns that prove broadly applicable get promoted from here into `patterns/`.
The rest stays as reference for understanding how mature projects solve specific problems.
## Files
- `golang.md` — conventions from the golang/go source
- `golang-analysis.md` — deeper analysis of golang/go source architecture
- `prometheus.md` — patterns from prometheus/prometheus (TSDB, storage, metrics)
+364
View File
@@ -0,0 +1,364 @@
# Go Language Source: Architectural Conventions
How does the Go team build Go itself? What does the language source
reveal about conventions, governance, and infrastructure decisions?
**Repo:** [golang/go](https://github.com/golang/go)
---
## 1. Repo Shape
| Metric | Value |
|--------|-------|
| Size | 632M |
| Go files | 11,245 |
| Assembly files (runtime) | 200 |
| Commits | 66,142 |
| Contributors | 2,842 |
| Test files | 1,811 |
| Non-test files | 6,065 |
| Test ratio | 1:3.3 |
| TODOs (non-test) | 3,428 (all owner-attributed) |
### Organizational Philosophy
```
src/
├── cmd/ # The toolchain (compile, link, go, gofmt, vet)
│ └── compile/
│ └── internal/ssa/ # 417,686 lines of SSA compiler
├── internal/ # 61 hidden packages (API firewall)
├── runtime/ # 129,141 lines (scheduler, GC, memory)
├── encoding/ # Serialization (json, xml, gob)
├── net/ # Networking
├── io/ # Stream interfaces
└── testing/ # Test framework
```
The Go source has three layers:
1. **Flat stdlib** — user-visible packages (`fmt`, `io`, `net`)
2. **`internal/`** — shared infrastructure hidden from users (61 packages)
3. **`cmd/`** — the toolchain itself (compiler, linker, build tool)
---
## 2. What the Codebase Values
### By import frequency
| Package | Imports | Role |
|---------|---------|------|
| `fmt` | 2,031 | Formatting (the universal tool) |
| `testing` | 1,658 | Tests are first-class citizens |
| `strings` | 1,454 | String manipulation |
| `os` | 1,306 | System interaction |
| `unsafe` | 1,304 | Low-level memory access |
| `runtime` | 970 | Runtime introspection |
| `io` | 924 | Stream abstraction |
**The surprise:** `unsafe` (1,304 imports) ranks 5th — nearly tied with
`os`. The language that preaches memory safety uses `unsafe` extensively
in its own implementation. This is the "know the rules so you know
where they don't apply" principle.
### By size (what consumes the most lines)
| Component | Lines | Nature |
|-----------|-------|--------|
| SSA compiler | 417,686 | Mostly generated rewrite rules |
| Runtime | 129,141 | Scheduler + GC + memory |
| `testing/` | 4,770 | Testing framework |
| `internal/poll` | 5,087 | OS I/O multiplexing |
| `internal/fuzz` | 4,220 | Fuzzing infrastructure |
| `encoding/json/v2` | 6,387 | The JSON rewrite (experiment) |
---
## 3. The Bootstrap Problem
**How does Go compile itself?**
Go has been self-hosting since May 2015 (Russ Cox, commit `0f4132c907`:
"all: build and use go tool compile, go tool link"). The bootstrap
chain:
1. A pre-built Go 1.4 binary compiles the bootstrap tools
2. Those tools compile the current Go toolchain
3. The new toolchain recompiles itself for correctness
**The `cmd/dist` tool** orchestrates this multi-stage build. Unlike
Elixir (which keeps 33 Erlang files permanently), Go eliminated its
non-Go dependencies entirely. The only external requirement is a
previous Go binary.
**Convention:** Self-hosting means the compiler, linker, and runtime
are all written in Go. The runtime includes 200 assembly files for
architecture-specific operations (scheduler context switches, atomic
operations, system calls).
---
## 4. TODO Culture: Owned Accountability
```go
// TODO(gri) — 320 occurrences (Robert Griesemer)
// TODO(mdempsky) — 198 occurrences (Matthew Dempsky)
// TODO(adonovan) — 170 occurrences (Alan Donovan)
// TODO(mknyszek) — 98 occurrences (Michael Knyszek)
// TODO(rsc) — 96 occurrences (Russ Cox)
```
**3,428 TODOs** in non-test code. Every TODO has an owner. The top 5
TODO authors are all core team members (compiler, runtime, and tools
leads).
**Convention:** `// TODO(username): description`
**Growth pattern:** linkname directives — 43 touches in 2019, 48 in
2022, 92 in 2024, 72 in 2025. The codebase is actively evolving its
relationship with internal/external boundaries.
**Contrast with Elixir:** Go TODOs are permanent documentation of known
limitations. Elixir TODOs are time-bombs with version deadlines. Go
accepts technical debt as a layer; Elixir refuses to let it accumulate.
---
## 5. Unique Patterns
### 5.1 `internal/` as API Firewall (61 packages)
Go's most distinctive structural pattern. The `internal/` directory is
enforced by the compiler — code outside the tree cannot import these
packages.
**Key internal packages:**
- `internal/godebug` — runtime feature flags (79 settings)
- `internal/goexperiment` — compile-time experiment guards
- `internal/singleflight` — dedup concurrent function calls
- `internal/bisect` — binary search for debugging (Russ Cox, 2023)
- `internal/poll` — OS I/O polling (5,087 lines)
- `internal/fuzz` — fuzzing coordinator (4,220 lines)
- `internal/coverage` — code coverage (3,821 lines)
**Convention:** Code shared between stdlib packages but not suitable for
public API goes in `internal/`. This is Go's answer to "how do you
share utilities without committing to backward compatibility."
### 5.2 GODEBUG: Runtime Feature Flags
Introduced 2021 (Brad Fitzpatrick), formalized as a compatibility
mechanism by Russ Cox in 2022 (proposal #56986: "extended backwards
compatibility for Go", 70 comments).
```go
var http2server = godebug.New("http2server")
func ServeConn(c net.Conn) {
if http2server.Value() == "0" {
// disable HTTP/2
}
}
```
**79 godebug settings** across the codebase. Each time a non-default
setting causes a behavior change, code calls `IncNonDefault()` to
increment a counter readable via `runtime/metrics`.
**Convention:** When a behavior change might break existing programs,
add a GODEBUG setting. The old behavior remains accessible via
`GODEBUG=setting=old_value`. New Go versions automatically use the
old behavior when building code that declared an older `go` directive
in `go.mod`.
**This is the Go compatibility promise made machine-enforceable.**
### 5.3 GOEXPERIMENT: Compile-Time Feature Gates
```go
// In internal/goexperiment/flags.go:
// GOEXPERIMENT=jsonv2 enables the new JSON API
```
**Convention:** Major API additions ship behind experiment flags.
`encoding/json/v2` (6,387 lines, Apr 2025) exists in the tree but is
only visible when `GOEXPERIMENT=jsonv2` is set. This allows the code
to be developed in-tree, tested by adventurous users, and refined
before the compatibility promise applies.
### 5.4 Compiler Directives as Hidden Language
```go
//go:linkname localFunction remote/package.Function
//go:nosplit
//go:nowritebarrier
//go:noescape
//go:systemstack
```
**1,711 `go:linkname` directives** and **2,428 runtime compiler
directives** in non-test code. These are effectively a hidden language
within Go — they bypass the type system, calling conventions, and
garbage collector safety for performance-critical paths.
**Convention:** Directives are ONLY acceptable in the runtime and
compiler. The Go team is actively trying to reduce `go:linkname` usage
(92 touches in 2024 — many are removals). Third-party packages that use
`go:linkname` to access internals are explicitly unsupported.
### 5.5 Generated Code: The SSA Compiler
The SSA (Static Single Assignment) compiler backend is 417,686 lines,
but the largest files are generated:
```
opGen.go — 97,135 lines (generated)
rewriteAMD64.go — 79,703 lines (generated)
rewritegeneric.go — 38,337 lines (generated)
rewriteARM64.go — 26,203 lines (generated)
```
**Convention:** Generated files are checked into the repo (not
generated at build time). They contain a `// Code generated` header.
The generators live alongside their output. This means `git blame`
works on generated code — you can trace when a rewrite rule was added.
### 5.6 The Runtime: 129K Lines of Go + Assembly
```
proc.go — 8,156 lines (THE goroutine scheduler)
malloc.go — 2,501 lines (memory allocator)
mgc.go — 2,315 lines (garbage collector)
mheap.go — 3,030 lines (heap management)
panic.go — 1,788 lines (panic/recover machinery)
```
The scheduler (`proc.go`) documents its own design at the top:
> "The main concepts are: G - goroutine. M - worker thread, or machine.
> P - processor, a resource that is required to execute Go code."
**Convention:** The runtime combines Go and assembly (200 `.s` files).
Architecture-specific operations (context switches, atomic ops, system
calls) are in assembly. Everything else is in Go, using compiler
directives to bypass safety checks where needed.
---
## 6. PR Discussion Patterns
Go uses GitHub issues for proposals, not PRs for discussion. The key
governance mechanism is the **proposal process** with designated
reviewers.
### json/v2 (Issue #71497, 201 comments, Jan 2025)
"The largest major revision of a standard Go package to date."
**Key design decision:** Split into `encoding/json/v2` (semantic) and
`encoding/jsontext` (syntactic). The syntactic layer has no reflection
dependency — it's a pure JSON tokenizer.
**Ship strategy:** Land behind `GOEXPERIMENT=jsonv2`, iterate with
community feedback, then graduate. The code lives in-tree but doesn't
count as a compatibility commitment until the experiment flag is
removed.
**External validation:** Built as `github.com/go-json-experiment/json`
first, iterated for years, then proposed for stdlib. The implementation
preceded the proposal.
### GODEBUG (Issue #56986, 70 comments, Nov 2022)
Russ Cox's proposal for machine-enforced backward compatibility.
**The problem:** Sort algorithm changes, bug fixes, and behavior
improvements can break programs that depend on old behavior.
**The solution:** `go.mod`'s `go` directive becomes a compatibility
declaration. Programs built with Go 1.22 but declaring `go 1.20`
automatically get Go 1.20 behavior for any setting that changed between
1.20 and 1.22.
**Lesson:** The Go team solved "how do you improve a language without
breaking users" with a general mechanism rather than case-by-case
migration. Each new GODEBUG setting is a structured backward
compatibility opt-out.
### Generics (Issue #15292, 874 comments, 2016-2021)
5 years of discussion. The most debated language change in Go's history.
**Lesson:** Committee-driven projects can take years on foundational
decisions because consensus requires addressing every edge case. Compare
to Elixir's formatter (1 hour, zero comments) — the BDFL model moves
faster but accepts more single-point-of-failure risk.
### slog (Issue #56345, 841 comments, 2022-2023)
Structured logging took 10 months and 841 comments to land.
**Lesson:** Even when the need is clear and the solution is well-known,
Go's process requires exhaustive discussion. The result is usually
better (slog is well-designed), but the cost is measured in months.
---
## 7. Cross-Ecosystem Comparisons
| Aspect | Go | Elixir |
|--------|-----|--------|
| TODOs | 3,428, owner-attributed, permanent | 127, version-gated, deadlines |
| Self-hosting | Complete since 2015 | 33 Erlang files permanently |
| Feature gates | GOEXPERIMENT (compile-time) | None (ship or don't) |
| Compat mechanism | GODEBUG (79 settings) | Deprecation → removal on version |
| Governance | Committee (proposals, 874-comment threads) | BDFL (José, 1-hour merges) |
| Internal boundary | `internal/` (compiler-enforced, 61 packages) | OTP applications (convention-enforced) |
| Generated code | Checked in (97K-line files) | Compile-time (no artifacts) |
| Assembly | 200 .s files in runtime | None (delegates to Erlang/BEAM) |
| Biggest file | 97,135 lines (generated) | 7,102 lines (Kernel) |
---
## 8. What This Teaches
1. **`internal/` solves "shared but not public" at the language level.**
61 packages that other ecosystems have to solve with conventions,
Go solves with compiler enforcement. This is why Go projects
rarely have "utils" packages that leak abstraction — the pattern
exists in the language itself.
2. **GODEBUG is the most sophisticated backward compatibility mechanism
in any language runtime.** It makes the compatibility promise
*machine-verifiable* rather than *socially-enforced*. Programs don't
just get old behavior by default — they get it because their `go.mod`
declares what era they belong to.
3. **GOEXPERIMENT enables fearless iteration in the stdlib.** json/v2
can exist in-tree, be tested, be refined — all without triggering
the compatibility promise. This is "feature flags for a language."
4. **3,428 TODOs is honest, not sloppy.** Each one has an owner. They
document known limitations rather than hiding them. Go prefers
"explicitly imperfect" over "implicitly broken."
5. **Compiler directives are a hidden language.** 4,139 directives
(linkname + runtime pragmas) bypass Go's safety model. The Go team
accepts that the runtime needs a different language than users —
but actively restricts this power from escaping to third-party code.
6. **Generated code checked in > generated at build time** for
archaeology. `git blame` on `rewriteAMD64.go` tells you when a
codegen rule was added and why. Build-time generation loses this
history.
7. **Committee governance produces better designs but 10x slower.** Go's
slog (841 comments, 10 months) vs Elixir's formatter (0 comments,
1 hour). The designs are comparable in quality — the process cost
is where they differ.
8. **`unsafe` in Go's own source (1,304 imports) proves that safety
rules are for users, not for runtime implementors.** The people who
wrote the safety rules know exactly where they don't apply.
<!-- PATTERN_COMPLETE -->
+270
View File
@@ -0,0 +1,270 @@
# Go Language Source: Convention Reference
Quick-reference for conventions extracted from the golang/go source
code. Each entry: pattern name, location, example, when to use,
when NOT to use, origin.
---
## Owner-Attributed TODOs
**Location:** Throughout `src/`
```go
// TODO(gri): consider using a different approach here
// TODO(rsc): this should be cleaned up in the next release
```
**When to use:** Known limitations that a specific person should
address. The owner tag creates accountability without creating an
issue (issues are for user-visible problems; TODOs are for internal
engineering debt).
**When NOT to use:** User-visible bugs (file an issue instead).
Aspirational improvements with no clear owner. Anything that should
block a release.
**Origin:** Convention since Go's earliest commits. The Go team
averages 3,428 TODOs across the codebase — this is a conscious
engineering culture, not neglect.
---
## `internal/` Packages
**Location:** `src/internal/` (61 packages)
```go
// internal/singleflight — dedup concurrent calls
// internal/godebug — runtime feature flags
// internal/poll — OS I/O polling
// internal/bisect — binary search debugging
```
**When to use:** Code shared between stdlib packages that should NOT
become public API. Utility code that isn't stable enough for the
compatibility promise. Implementation details that users shouldn't
depend on.
**When NOT to use:** Code that external packages need. Code that's
stable enough for public API (promote it). One-off helpers that only
one package uses (keep them package-private).
**Origin:** `src/internal/` existed since Go moved sources from
`src/pkg` to `src` (2014). The compiler enforces the import restriction
— no code outside the tree can import internal packages.
---
## GODEBUG: Runtime Feature Flags
**Location:** `internal/godebug/godebug.go` (316 lines)
```go
var http2server = godebug.New("http2server")
func ServeConn(c net.Conn) {
if http2server.Value() == "0" {
// user opted out of HTTP/2
}
// IncNonDefault must be called each time non-default behavior fires
http2server.IncNonDefault()
}
```
**When to use:** Behavior changes that might break existing programs.
The old behavior becomes accessible via `GODEBUG=setting=value`. Tied
to `go.mod`'s `go` directive for automatic version-based defaults.
**When NOT to use:** Bug fixes that no reasonable program depends on.
New features (use GOEXPERIMENT instead). Performance optimizations that
don't change observable behavior.
**Origin:** Brad Fitzpatrick added the package in Aug 2021. Russ Cox
formalized it as the backward compatibility mechanism in proposal
#56986 (Nov 2022, 70 comments). 79 settings now exist.
---
## GOEXPERIMENT: Compile-Time Feature Gates
**Location:** `internal/goexperiment/flags.go` (136 lines)
```go
// Set via: GOEXPERIMENT=jsonv2 go build
// Check via build tag: //go:build goexperiment.jsonv2
```
**When to use:** Major new APIs or behavior changes that need real-world
testing before committing to the compatibility promise. The code lives
in-tree but isn't visible without the flag.
**When NOT to use:** Small features that can ship directly. Bug fixes.
Internal refactoring. Anything that doesn't need user feedback before
committing.
**Origin:** The GOEXPERIMENT mechanism predates its formalization — used
for fieldtrack, regabi, unified. json/v2 (Apr 2025) is the highest-
profile use: 6,387 lines shipped behind a flag.
---
## Compiler Directives
**Location:** Throughout `runtime/` and `cmd/`
```go
//go:linkname localName remote/pkg.ExportedName
//go:nosplit
//go:nowritebarrier
//go:noescape
//go:systemstack
```
**When to use:** ONLY in the runtime and compiler. `linkname` accesses
unexported symbols across packages. `nosplit` prevents stack growth
checks. `nowritebarrier` asserts no GC barriers. `systemstack` forces
execution on the system stack.
**When NOT to use:** In application code. In stdlib packages outside
runtime. In third-party packages (explicitly unsupported — the Go team
actively removes `go:linkname` targets that external packages depend
on).
**Origin:** 1,711 `go:linkname` and 2,428 other runtime directives.
The Go team is actively trying to reduce linkname usage (92 touches
in 2024, many removals).
---
## Generated Code (Checked In)
**Location:** `cmd/compile/internal/ssa/`
```go
// Code generated by ssa/gen/*.go; DO NOT EDIT.
// opGen.go — 97,135 lines
// rewriteAMD64.go — 79,703 lines
// rewritegeneric.go — 38,337 lines
```
**When to use:** When the generated output needs `git blame` history.
When generators should be auditable alongside their output. When build
reproducibility matters (no external tool dependency).
**When NOT to use:** When the generated output is large AND changes
frequently (diff noise). When the generator is trivial (just use
`go generate` at build time).
**Origin:** SSA compiler introduced on dev.ssa branch (2015). The
rewrite rules are generated from a DSL but checked in so reviewers
can see exactly what changed.
---
## Assembly in Runtime
**Location:** `runtime/*.s` (200 files)
```asm
// runtime/asm_amd64.s
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
...
```
**When to use:** Context switches, atomic operations, system calls,
and anything that needs direct control over registers/stack. Each
architecture has its own set of assembly files.
**When NOT to use:** Anything that can be written in Go with acceptable
performance. The Go team actively converts assembly to Go where
possible (e.g., crypto packages moving from asm to Go+intrinsics).
**Origin:** Runtime has always included assembly — Go's scheduler and
GC require direct hardware control that no high-level language can
provide.
---
## Proposal Process (Committee Governance)
**Location:** GitHub issues, not PRs
```
Issue #71497 (json/v2): 201 comments, Jan 2025
Issue #56986 (GODEBUG): 70 comments, Nov 2022
Issue #56345 (slog): 841 comments, 10 months
Issue #15292 (generics): 874 comments, 5 years
```
**When to use:** Any user-visible API addition or behavior change.
The proposal process ensures all edge cases are considered before
committing to the compatibility promise.
**When NOT to use:** Internal refactoring, performance improvements
that don't change API, bug fixes. These go through normal code review
(Gerrit/GitHub).
**Origin:** The Go proposal process evolved from informal (early Go)
to formalized (2015+). Russ Cox and the Go team manage a proposal
review meeting that triages and decides.
---
## The Scheduler as Documentation
**Location:** `runtime/proc.go` (8,156 lines)
```go
// Goroutine scheduler
// The scheduler's job is to distribute ready-to-run goroutines over
// worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
```
**When to use:** Complex algorithms should document their model at the
top of the file. The Go runtime uses extensive comments to explain
the scheduler's invariants, the GC's phases, and the memory
allocator's structure.
**When NOT to use:** Simple code that's self-documenting. Comments
that restate what the code does rather than WHY it does it.
**Origin:** The G-M-P model comment dates to the scheduler rewrite
(2014). The convention of architectural documentation at file-top
extends throughout the runtime.
---
## json/v2: Experiment-to-Stdlib Pipeline
**Location:** `encoding/json/v2/` (6,387 lines, 13 files)
```go
// encoding/json/v2 — semantic layer (uses reflection)
// encoding/jsontext — syntactic layer (no reflection dependency)
```
**When to use:** When building a major stdlib revision. The pattern:
1. Build as external module (`go-json-experiment/json`)
2. Iterate with real users for years
3. Propose for stdlib with working implementation
4. Ship behind GOEXPERIMENT flag
5. Graduate when stable
**When NOT to use:** Small additions that don't need years of
iteration. Features that can be backward-compatible additions to
existing packages.
**Origin:** `go-json-experiment/json` existed since 2022. Proposed as
#71497 (Jan 2025, 201 comments). Landed behind flag Apr 2025 (commit
`0e17905793` by Damien Neil).
<!-- PATTERN_COMPLETE -->
+182
View File
@@ -0,0 +1,182 @@
# Patterns Extracted from prometheus/prometheus
## Pattern: Atomic File Operations with Suffix Convention
**Source:** `tsdb/db.go`
**Category:** storage
**What:** Use directory suffixes (`.tmp-for-creation`,
`.tmp-for-deletion`) to make multi-step file operations
crash-safe. On startup, clean up any dirs with these
suffixes (they represent incomplete operations).
**Why:** Database storage needs atomicity. If the process
crashes between creating a block and finalizing it, you
need to know the block is incomplete. The suffix convention
makes incomplete state visible at the filesystem level
without requiring a separate journal.
**Example:**
```go
const (
tmpForDeletionBlockDirSuffix = ".tmp-for-deletion"
tmpForCreationBlockDirSuffix = ".tmp-for-creation"
)
// On startup: remove any .tmp-* dirs (incomplete ops)
// On create: write to dir.tmp-for-creation, then rename
// On delete: rename to dir.tmp-for-deletion, then remove
```
**When to use:** Any system that manages files/directories
and needs crash consistency without a full WAL. Simpler
than a write-ahead log for coarse-grained operations.
**When NOT to use:** When you already have a WAL or
transaction log. Or for fine-grained operations where
rename semantics are insufficient.
---
## Pattern: DefaultOptions() Function
**Source:** `tsdb/db.go`
**Category:** configuration
**What:** Provide a `DefaultOptions()` function returning a
fully-populated config struct. Users copy and override only
what they need. No nil-means-default ambiguity.
**Why:** Large config structs (20+ fields) are unwieldy.
By providing sane defaults as a function (not a
package-level var), you avoid mutation bugs and make it
clear what "normal" looks like. Users only specify
deviations.
**Example:**
```go
func DefaultOptions() *Options {
return &Options{
WALSegmentSize: wlog.DefaultSegmentSize,
RetentionDuration: int64(15*24*time.Hour / ...),
MinBlockDuration: DefaultBlockDuration,
MaxBlockDuration: DefaultBlockDuration,
SamplesPerChunk: DefaultSamplesPerChunk,
// ... 20 more fields with sane defaults
}
}
// Usage:
opts := tsdb.DefaultOptions()
opts.RetentionDuration = 30 * 24 * time.Hour
db, err := tsdb.Open(dir, nil, nil, opts, nil)
```
**When to use:** Config structs with many fields where most
users want defaults. Especially when zero-value semantics
would be confusing (e.g., 0 retention = infinite? or off?).
**When NOT to use:** Small configs (3-4 fields) where
struct literal with zero-means-default is clear enough.
---
## Pattern: Scrape Loop with Aligned Timestamps
**Source:** `scrape/scrape.go`
**Category:** concurrency
**What:** Periodic scrape loops that align timestamps to
intervals with a small tolerance, enabling better storage
compression downstream.
**Why:** Time-series databases compress better when
timestamps are regular. A 2ms tolerance on alignment
means scraped data aligns to the expected grid while
accommodating real-world jitter.
**Example:**
```go
var ScrapeTimestampTolerance = 2 * time.Millisecond
var AlignScrapeTimestamps = true
// In scrape loop: if scrape finishes within tolerance
// of expected timestamp, snap to the grid
```
**When to use:** Any periodic data collection where
downstream storage benefits from timestamp regularity.
Metrics, heartbeats, polling loops.
**When NOT to use:** Event-driven data where timestamps
must reflect actual occurrence time. Audit logs, user
actions, financial transactions.
---
## Pattern: Sentinel Errors with Interface Check
**Source:** `tsdb/db.go`
**Category:** error-handling
**What:** Define package-level sentinel errors with
`errors.New()` and use compile-time interface assertions
to verify implementations satisfy storage interfaces.
**Why:** `ErrNotReady` as a sentinel lets callers use
`errors.Is` for retry logic. The pattern ensures error
identity is stable across versions (not string-matched).
**Example:**
```go
var ErrNotReady = errors.New("TSDB not ready")
// Callers can reliably detect this:
if errors.Is(err, tsdb.ErrNotReady) {
// Retry later — DB is still initializing
}
```
**When to use:** Any error that callers need to handle
programmatically (retry, fallback, special UI). Make it a
named sentinel, not a string comparison.
**When NOT to use:** Errors that are always terminal or
always logged-and-discarded. Not every error needs a name.
---
## Pattern: Compile-Time Interface Satisfaction
**Source:** `scrape/scrape.go`
**Category:** organization
**What:** Use `var _ Interface = (*Type)(nil)` to verify at
compile time that a type satisfies an interface, even if
the type is only used dynamically.
**Why:** Without this, you discover missing methods only
when the type is actually used — which might be in a
rarely-exercised code path or only in production. The
compile-time check catches it immediately.
**Example:**
```go
var _ FailureLogger = (*logging.JSONFileLogger)(nil)
// Fails at compile time if JSONFileLogger doesn't
// implement FailureLogger
```
**When to use:** Any type that implements an interface
consumed dynamically (registered in a map, stored as
interface value, passed to framework code).
**When NOT to use:** Types whose interface satisfaction is
already enforced by direct usage in the same package.
<!-- PATTERN_COMPLETE -->