Compare commits

..
1 Commits
15 changed files with 1613 additions and 3135 deletions
-17
View File
@@ -1,17 +0,0 @@
# 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.
## Structure
- `patterns/` — Go stdlib patterns (interfaces, errors, concurrency, structs, testing, docs, style, API conventions, packages)
- `kubernetes/` — Production-scale patterns from Kubernetes (controllers, informers, workqueues)
- `comparison/` — stdlib vs Kubernetes patterns
- `smells/` — Anti-patterns and common Go mistakes
- `changelog/` — Daily digest of merged PRs
## Philosophy
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.
When unsure how to do something in Go, look at how the standard library does it.
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.
-374
View File
@@ -1,374 +0,0 @@
# API Conventions in the Go Standard Library
## 1. The Must Pattern
**Pattern name:** MustXxx (Panic on Error)
**Source citation:** `regexp/regexp.go` lines 310–320, `text/template/helper.go` lines 19–30
**What it does:** A function wraps a fallible constructor and panics if the error
is non-nil. Named `MustXxx` or `Must` (when wrapping a generic `(T, error)` pair).
**Why:** Safe initialization of package-level variables at program startup. Since
`var` initializers can't handle errors, `Must` converts programmer errors (bad
regex literals, bad templates) into immediate panics that surface during init.
**Anti-pattern:** Using Must in runtime code where the input is dynamic/user-provided;
panicking on recoverable errors; naming it something other than Must (e.g., `PanicOnError`).
**Code examples from source:**
```go
// regexp/regexp.go:310-320
// MustCompile is like [Compile] but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
```
```go
// text/template/helper.go:19-30
// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
// var t = template.Must(template.New("name").Parse("text"))
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}
```
---
## 2. Compile / MustCompile Pair
**Pattern name:** Fallible Constructor + Must Wrapper
**Source citation:** `regexp/regexp.go` lines 130–131, 310–320
**What it does:** The real constructor returns `(*T, error)`. A parallel `Must` variant
wraps it for use in global variable initialization.
**Why:** Separates concerns: `Compile` is for runtime use where errors are handled;
`MustCompile` is for compile-time-known values where failure is a programming bug.
**Anti-pattern:** Only providing the Must variant (no way to handle errors gracefully);
only providing the error variant (verbose for package-level vars).
**Code example from source:**
```go
// regexp/regexp.go:130-131
func Compile(expr string) (*Regexp, error) {
return compile(expr, syntax.Perl, false)
}
// regexp/regexp.go:310-315
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
```
---
## 3. XxxWithContext Variant
**Pattern name:** WithContext Function Overload
**Source citation:** `net/http/request.go` lines 867–869, 894–930
**What it does:** Provides two function variants: `NewRequest` (uses `context.Background()`)
and `NewRequestWithContext` (accepts an explicit context). The simple version delegates
to the context-aware one.
**Why:** Context was added after the original API was established. The `WithContext`
variant enables cancellation and deadlines; the plain variant preserves backward
compatibility and ergonomics for the common case.
**Anti-pattern:** Breaking the existing API signature; always requiring context even
for fire-and-forget uses; naming it `NewRequestCtx`.
**Code example from source:**
```go
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
// net/http/request.go:894+
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// full implementation...
}
```
---
## 4. Nil-Opts Convention (Optional Config Pointer)
**Pattern name:** `*Options` Parameter — Nil Means Defaults
**Source citation:** `log/slog/text_handler.go` lines 28–42, `log/slog/handler.go` lines 135–175
**What it does:** A constructor accepts a pointer to an options struct. If the pointer
is nil, all defaults apply. The constructor internally substitutes a zero-value struct.
**Why:** Keeps the simple case clean (`NewTextHandler(os.Stderr, nil)`) while allowing
full customization. The pointer type signals "this entire argument is optional."
**Anti-pattern:** Requiring a non-nil options struct even with zero customization;
using variadic functional options when a simple struct suffices.
**Code example from source:**
```go
// log/slog/text_handler.go:28-42
// NewTextHandler creates a [TextHandler] that writes to w,
// using the given options.
// If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
return &TextHandler{
&commonHandler{
json: false,
w: w,
opts: *opts,
mu: &sync.Mutex{},
},
}
}
```
---
## 5. Builder Pattern (Accumulate + Finalize)
**Pattern name:** Builder (Write Methods + String/Bytes Finalizer)
**Source citation:** `strings/builder.go` lines 14–113
**What it does:** A zero-value struct accumulates data via Write/WriteByte/WriteString
methods, then produces a final result via String(). The builder is not reusable after
a copyCheck-protected modification.
**Why:** Avoids repeated string concatenation (O(n²) allocations). The zero value is
ready to use. Implements `io.Writer` so it integrates with `fmt.Fprintf`, etc.
**Anti-pattern:** Allocating on every append; requiring explicit initialization;
not implementing standard interfaces (`io.Writer`).
**Code example from source:**
```go
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
// strings/builder.go:92-96
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
// strings/builder.go:48-50
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
```
---
## 6. Layered API (Convenience → Full Control)
**Pattern name:** Convenience Wrappers over Configurable Core
**Source citation:** `os/file.go` lines 385–415
**What it does:** Simple functions (`Open`, `Create`) delegate to the fully configurable
`OpenFile` with pre-set flags. Users choose their level of control.
**Why:** 90% of file opens are reads or creates. Layered APIs serve the common case
without hiding power. The naming makes intent clear.
**Anti-pattern:** Only exposing the full-power version; making users learn flag
constants for simple reads; duplicating implementation across convenience functions.
**Code example from source:**
```go
// os/file.go:389-393
// Open opens the named file for reading.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
// os/file.go:399-403
// Create creates or truncates the named file.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
// os/file.go:410+ (the general form)
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
// ...
}
```
---
## 7. Package-Level Functions Delegating to DefaultXxx
**Pattern name:** Convenience Package Functions
**Source citation:** `net/http/client.go` line 109, implied by `http.Get`, `http.Post`
**What it does:** Top-level functions like `http.Get(url)` call methods on the
`DefaultClient`. Users can bypass by creating their own `Client`.
**Why:** Makes the simple case trivial (one-liner HTTP requests). No import of
constructors or setup needed. The package "just works" for basic usage.
**Anti-pattern:** Not providing convenience functions (forcing explicit construction
even for prototyping); making the default's behavior non-obvious.
**Code example from source:**
```go
// net/http/client.go:109
var DefaultClient = &Client{}
// net/http/client.go (implied pattern):
// func Get(url string) (resp *Response, err error) {
// return DefaultClient.Get(url)
// }
```
---
## 8. Register Pattern (Pluggable Algorithms)
**Pattern name:** RegisterXxx for Side-Effect Imports
**Source citation:** `crypto/crypto.go` lines 145–150
**What it does:** A `RegisterHash(h Hash, f func() hash.Hash)` function allows
algorithm implementations in sub-packages to register themselves via `init()`.
The main package dispatches based on the registered factories.
**Why:** Decouples the algorithm registry from specific implementations. Users import
only the algorithms they need (e.g., `_ "crypto/sha256"`). Reduces binary size and
avoids circular dependencies.
**Anti-pattern:** Hard-coding all implementations; requiring explicit constructor calls
for each algorithm; using global mutable state without clear ownership.
**Code example from source:**
```go
// crypto/crypto.go:145-150
func RegisterHash(h Hash, f func() hash.Hash) {
if h == 0 || h >= maxHash {
panic("crypto: RegisterHash of unknown hash function")
}
hashes[h] = f
}
```
---
## 9. Graceful Shutdown Pattern
**Pattern name:** Close vs Shutdown (Immediate vs Graceful)
**Source citation:** `net/http/server.go` lines 3171–3220 (Close), 3221+ (Shutdown)
**What it does:** Provides both `Close()` (immediate, forceful) and `Shutdown(ctx)`
(graceful, waits for in-flight requests). The context on Shutdown provides a
timeout mechanism.
**Why:** Different operational scenarios need different termination semantics.
Graceful shutdown is critical for production services; immediate close is needed for
tests and emergency stops.
**Anti-pattern:** Only providing one shutdown mode; not accepting a context for
timeout control; leaking goroutines on shutdown.
**Code example from source:**
```go
// net/http/server.go:3171-3175
func (s *Server) Close() error {
s.inShutdown.Store(true)
s.mu.Lock()
defer s.mu.Unlock()
err := s.closeListenersLocked()
// ... forcefully closes all active connections
}
// net/http/server.go:3221+
// Shutdown gracefully shuts down the server without interrupting any
// active connections.
func (s *Server) Shutdown(ctx context.Context) error {
s.inShutdown.Store(true)
// ... closes listeners, waits for idle, respects ctx deadline
}
```
---
## 10. Channel-Based Timer/Ticker API
**Pattern name:** NewXxx Returning Channel-Bearing Struct
**Source citation:** `time/tick.go` lines 16–45, `time/sleep.go` lines 89–155
**What it does:** `NewTicker(d)` and `NewTimer(d)` return structs with a `C <-chan Time`
field. Consumers select on the channel to receive time events.
**Why:** Integrates time-based events with Go's concurrency primitives (select).
The channel-based API composes naturally with other goroutine patterns.
**Anti-pattern:** Callback-based timer APIs that don't compose with select; exposing
the send side of the channel; not documenting goroutine safety.
**Code example from source:**
```go
// time/tick.go:16-18
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
initTicker bool
}
// time/tick.go:36-45
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic("non-positive interval for NewTicker")
}
c := make(chan Time, 1)
t := (*Ticker)(unsafe.Pointer(newTimer(when(d), int64(d), sendTime, c, syncTimer(c))))
t.C = c
return t
}
```
+182 -70
View File
@@ -29,6 +29,11 @@ type Locker interface {
func (m *Mutex) Lock() {
m.mu.Lock()
}
// src/sync/mutex.go:64-67
func (m *Mutex) Unlock() {
m.mu.Unlock()
}
```
### Why
@@ -55,11 +60,12 @@ type Config struct {
mu sync.Mutex
data map[string]string
}
c2 := *c1 // COPIES the mutex — data race
c2 := *c1 // COPIES the mutex — data race waiting to happen
// DON'T: Forget defer
mu.Lock()
doSomething() // if this panics, mutex stays locked forever
// if this panics, the mutex stays locked forever
doSomething()
mu.Unlock()
```
@@ -97,7 +103,7 @@ func (o *Once) doSlow(f func()) {
### Why
The implementation reveals a subtle guarantee: **when Do returns, f has finished**. The naive CAS-only approach (documented in comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait.
The implementation reveals a subtle guarantee: **when Do returns, f has finished**. The naive CAS-only approach (documented in the comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait.
The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27).
@@ -120,6 +126,18 @@ func GetDB() *DB {
### Anti-pattern
```go
// DON'T: Implement once yourself with a bool
var initialized bool
var mu sync.Mutex
func init() {
mu.Lock()
if !initialized {
// ... setup ...
initialized = true
}
mu.Unlock()
}
// DON'T: Call Do recursively (deadlocks)
var once sync.Once
once.Do(func() {
@@ -135,8 +153,12 @@ once.Do(func() {
```go
// src/sync/waitgroup.go:14-43
// Typically, a main goroutine will start tasks by calling WaitGroup.Go
// and then wait for all tasks to complete by calling WaitGroup.Wait:
// A WaitGroup is a counting semaphore typically used to wait
// for a group of goroutines or tasks to finish.
//
// Typically, a main goroutine will start tasks, each in a new
// goroutine, by calling WaitGroup.Go and then wait for all tasks to
// complete by calling WaitGroup.Wait. For example:
//
// var wg sync.WaitGroup
// wg.Go(task1)
@@ -158,8 +180,7 @@ func (wg *WaitGroup) Go(f func()) {
go func() {
defer func() {
if x := recover(); x != nil {
// Don't call Done — let panic propagate fatally.
panic(x)
panic(x) // don't call Done — let panic propagate
}
wg.Done()
}()
@@ -170,7 +191,7 @@ func (wg *WaitGroup) Go(f func()) {
### Why
`WaitGroup.Go` encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit before the panic stack trace prints.
`WaitGroup.Go` (new in Go 1.25) encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit.
### Classic Pattern (pre-Go 1.25)
@@ -198,6 +219,14 @@ for _, item := range items {
}()
}
wg.Wait()
// DON'T: Forget Done (Wait blocks forever)
wg.Add(1)
go func() {
process()
// forgot wg.Done()
}()
wg.Wait() // hangs
```
---
@@ -208,17 +237,15 @@ wg.Wait()
```go
// src/sync/pool.go:44-63
// A Pool is a set of temporary objects that may be individually saved and
// retrieved.
//
// Any item stored in the Pool may be removed automatically at any time without
// notification. If the Pool holds the only reference when this happens, the
// item might be deallocated.
//
// 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
// build efficient, thread-safe free lists.
//
// An appropriate use of a Pool is to manage a group of temporary items
// silently shared among and potentially reused by concurrent independent
// clients of a package. Pool provides a way to amortize allocation overhead
// across many clients.
//
// An example of good use of a Pool is in the fmt package, which maintains a
// dynamically-sized store of temporary output buffers.
// relieving pressure on the garbage collector.
type Pool struct {
noCopy noCopy
local unsafe.Pointer
@@ -231,7 +258,7 @@ type Pool struct {
### Why
Pool is **not** a general cache. Items can vanish between GC cycles. Use for reducing allocation pressure on hot paths.
Pool is **not** a general cache. Items can vanish between GC cycles. It's for reducing allocation pressure on hot paths — `fmt` uses it for print buffers, `encoding/json` for encoder state.
### Idiomatic Usage (from fmt package)
@@ -259,10 +286,10 @@ func (p *pp) free() {
var connPool = sync.Pool{
New: func() any { return connectToDB() },
}
// Connections may be GC'd — use database/sql's pool instead
// Connections may be GC'd at any time — use database/sql's pool instead
// DON'T: Put dirty objects back without resetting
pool.Put(buf) // still has data from last use
pool.Put(buf) // still has data from last use — memory leak or data leak
```
---
@@ -279,8 +306,13 @@ Done() <-chan struct{}
// src/io/pipe.go:42-45
type pipe struct {
wrMu sync.Mutex
wrCh chan []byte
rdCh chan int
once sync.Once
done chan struct{} // closed on pipe close
rerr onceError
werr onceError
}
```
@@ -301,10 +333,10 @@ case result := <-work:
```go
// DON'T: Use chan bool for done signals
done := make(chan bool) // wastes 1 byte, true/false meaningless
done := make(chan bool) // wastes 1 byte per signal, true/false meaningless
// DON'T: Send to done (only unblocks one receiver)
done <- struct{}{}
// DON'T: Send to done (only works once, only one receiver)
done <- struct{}{} // only unblocks one goroutine
// DO: Close the channel (broadcasts to all)
close(done)
@@ -312,12 +344,15 @@ close(done)
---
## 6. Context Propagation Rules
## 6. Context Propagation
### Source: `src/context/context.go:37-48`
### Source: `src/context/context.go:37-48` (rules), `src/net/http/request.go:368-380`
From the package doc:
```go
// src/context/context.go:37-48
// Programs that use Contexts should follow these rules:
//
// 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
// parameter, typically named ctx:
@@ -330,19 +365,34 @@ close(done)
// if you are unsure about which Context to use.
```
### Request context in net/http:
```go
// src/net/http/request.go:368-380
func (r *Request) WithContext(ctx context.Context) *Request {
if ctx == nil {
panic("nil context")
}
r2 := new(Request)
*r2 = *r
r2.ctx = ctx
return r2
}
```
### Why
Context flows **down** the call chain, never stored in structs. It carries deadlines and cancellation signals for the current request, not persistent state.
Context flows **down** the call chain, never stored in structs. `WithContext` returns a shallow copy — the original request is not mutated. This is the immutable-context pattern.
### Anti-pattern
```go
// DON'T: Store context in a struct
type Server struct {
ctx context.Context // stale context persists beyond request
ctx context.Context // stale context persists beyond request lifecycle
}
// DON'T: Pass nil
// DON'T: Pass nil context
doWork(nil, data) // use context.TODO() if unsure
// DON'T: Put context anywhere other than first parameter
@@ -351,62 +401,61 @@ func doWork(data Data, ctx context.Context) // wrong position
---
## 7. Context Cancellation with Timeout
## 7. Context Cancellation (WithCancel/WithTimeout)
### Source: `src/net/http/server.go:4007-4050` (TimeoutHandler)
### Source: `src/context/context.go:242-249` (WithCancel), `src/net/http/server.go:4007-4050` (TimeoutHandler)
```go
// src/net/http/server.go:4011-4050
// src/context/context.go:242-249
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
```
Real-world use — net/http TimeoutHandler:
```go
// src/net/http/server.go:4011-4014
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
defer cancelCtx()
r = r.WithContext(ctx)
done := make(chan struct{})
panicChan := make(chan any, 1)
// ...
go func() {
defer func() {
if p := recover(); p != nil {
panicChan <- p
}
}()
h.handler.ServeHTTP(tw, r)
close(done)
}()
select {
case p := <-panicChan:
panic(p)
case <-done:
// handler completed — copy response
// handler completed
case <-ctx.Done():
// timeout — write 503
// timeout
}
}
```
### Why
This is the full pattern: context with timeout + goroutine + select on done/timeout/panic. Key details:
1. `defer cancelCtx()` — always release resources
2. Panic propagation via dedicated channel
3. Select on three outcomes: success, timeout, panic
`defer cancelCtx()` is critical — it releases resources (timers, goroutines) when the parent returns, even if the child hasn't timed out yet. The go vet tool checks for this.
### Anti-pattern
```go
// DON'T: Forget to call cancel (leaks timer goroutines)
ctx, _ := context.WithTimeout(parent, 5*time.Second)
// DON'T: Forget to call cancel (leaks goroutines)
ctx, _ := context.WithCancel(parent) // cancel function discarded!
// DON'T: Ignore context in long operations
func longWork(ctx context.Context) {
time.Sleep(10 * time.Minute) // ignores cancellation
}
// DON'T: Cancel before work starts
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
cancel() // immediately cancels — no work can happen
doWork(ctx)
```
---
## 8. Select with Non-Blocking Check
## 8. Select with Done Channel
### Source: `src/io/pipe.go:51-60`
### Source: `src/context/context.go:83-100` (Done in select), `src/io/pipe.go:51-60`
```go
// src/io/pipe.go:51-60
@@ -430,7 +479,26 @@ func (p *pipe) read(b []byte) (n int, err error) {
### Why
The double-select pattern: first a non-blocking check (with `default`), then a blocking wait. The non-blocking check prevents a race where `done` was closed between the last operation and entering the blocking select.
The double-select pattern: first a non-blocking check (with `default`), then a blocking wait. The non-blocking check prevents a race where `done` was closed between the last operation and the current one.
### Standard Context Select Pattern
```go
// From context package doc (line 83-100)
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
```
### Anti-pattern
@@ -442,29 +510,71 @@ for {
return
default:
}
// busy-spins CPU at 100%!
// busy-spins CPU at 100%
}
```
---
## 9. Channel Pipeline (io.Pipe)
## 9. Goroutine-per-Connection (net/http Server)
### Source: `src/net/http/server.go` (conceptual — the serve loop spawns goroutines per connection)
```go
// The pattern (simplified from server.go serve loop):
for {
conn, err := listener.Accept()
if err != nil {
// handle
continue
}
go srv.handleConn(conn) // one goroutine per connection
}
```
### Why
Go's goroutines are cheap (~2KB initial stack). The server doesn't need a thread pool or async/await — it spawns a goroutine per connection and lets the runtime scheduler handle multiplexing.
### Anti-pattern
```go
// DON'T: Limit yourself to a fixed thread pool for I/O-bound work
pool := make(chan struct{}, 10) // artificial limit on connections
for {
pool <- struct{}{} // blocks at 10
conn := accept()
go func() {
defer func() { <-pool }()
handle(conn)
}()
}
// Only appropriate for CPU-bound work or resource-constrained scenarios
```
---
## 10. Channel as Synchronous Pipe (io.Pipe)
### Source: `src/io/pipe.go:38-45`, `src/io/pipe.go:195-205`
```go
// src/io/pipe.go:38-45
type pipe struct {
wrMu sync.Mutex
wrCh chan []byte // writer sends data slices
rdCh chan int // reader returns bytes consumed
once sync.Once
done chan struct{}
rerr onceError
werr onceError
}
// src/io/pipe.go:195-205
func Pipe() (*PipeReader, *PipeWriter) {
pw := &PipeWriter{r: PipeReader{pipe: pipe{
wrCh: make(chan []byte), // unbuffered
rdCh: make(chan int), // unbuffered
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}}}
return &pw.r, pw
@@ -473,9 +583,9 @@ func Pipe() (*PipeReader, *PipeWriter) {
### Why
`io.Pipe` uses **unbuffered channels** — each Write blocks until Read consumes. Backpressure is automatic. The `done` channel signals shutdown.
`io.Pipe` connects a Writer to a Reader using **unbuffered channels** — each Write blocks until the corresponding Read consumes the data. No internal buffering means backpressure is automatic. The `done` channel signals when either end closes.
### Pipeline Pattern Template
### Pattern: Channel Pipeline
```go
func generate(ctx context.Context) <-chan int {
@@ -504,7 +614,7 @@ func produce() <-chan int {
for i := 0; i < 10; i++ {
ch <- i
}
// forgot close(ch) — range receivers hang
// forgot close(ch) — receivers hang on range
}()
return ch
}
@@ -512,7 +622,7 @@ func produce() <-chan int {
---
## 10. Background Worker with Context Shutdown
## 11. database/sql Connection Opener Goroutine
### Source: `src/database/sql/sql.go:836-843`
@@ -523,6 +633,7 @@ func OpenDB(c driver.Connector) *DB {
db := &DB{
connector: c,
openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string),
stop: cancel,
}
go db.connectionOpener(ctx)
@@ -532,22 +643,22 @@ func OpenDB(c driver.Connector) *DB {
### Why
A dedicated background goroutine processes work from a buffered channel. It's controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down. This is the standard "long-lived worker goroutine with graceful shutdown" pattern.
A dedicated background goroutine (`connectionOpener`) processes connection requests from a buffered channel. The goroutine is controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down cleanly. This is the "long-lived worker goroutine with context shutdown" pattern.
### Anti-pattern
```go
// DON'T: Start goroutines without shutdown mechanism
// DON'T: Start background goroutines without shutdown mechanism
go func() {
for {
processWork() // runs forever, no way to stop
processWork() // runs forever, no way to stop it
}
}()
```
---
## 11. noCopy — Preventing Value Copies
## 12. noCopy — Preventing Value Copies at Vet Time
### Source: `src/sync/cond.go:120-126`
@@ -562,14 +673,14 @@ func (*noCopy) Unlock() {}
### Why
Embedding `noCopy` makes `go vet` report errors when the struct is copied by value. All sync primitives use this because copying a locked mutex or active WaitGroup is always a bug.
Embedding `noCopy` in a struct makes `go vet` report an error when the struct is copied. All sync primitives use this because copying a locked mutex or in-use WaitGroup is always a bug.
### Anti-pattern
```go
// DON'T: Pass sync types by value
func doWork(wg sync.WaitGroup) { // copies!
defer wg.Done() // operates on copy, not original
func doWork(wg sync.WaitGroup) { // copies the WaitGroup!
defer wg.Done()
}
// DO: Pass by pointer
@@ -591,5 +702,6 @@ func doWork(wg *sync.WaitGroup) {
| Signal completion/cancellation | `chan struct{}` + `close()` |
| Deadline/timeout propagation | `context.WithTimeout` / `context.WithCancel` |
| Backpressure between producer/consumer | Unbuffered channels |
| Fan-out with results | Buffered channel + WaitGroup |
| Long-lived background worker | Goroutine + context cancellation |
| Prevent struct copying | Embed `noCopy` field |
-342
View File
@@ -1,342 +0,0 @@
# Documentation Patterns in the Go Standard Library
## 1. Package Documentation (doc.go or Package 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
**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
purpose, key types, and typical usage patterns.
**Why:** This is the first thing users see in `go doc <pkg>` and on pkg.go.dev. It
sets context, teaches the mental model, and provides copy-paste examples.
**Anti-pattern:** No package comment; package comment that just restates the package
name ("Package http provides http"); putting documentation in README instead of code.
**Code examples from source:**
```go
// net/http/doc.go:6-12
/*
Package http provides HTTP client and server implementations.
[Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests:
resp, err := http.Get("http://example.com/")
...
*/
```
```go
// os/file.go:5-43
// Package os provides a platform-independent interface to operating system
// functionality. The design is Unix-like, although the error handling is
// Go-like; failing calls return values of type error rather than error numbers.
// Often, more information is available within the error. For example,
// if a call that takes a file name fails, such as [Open] or [Stat], the error
// will include the failing file name when printed and will be of type
// [*PathError], which may be unpacked for more information.
```
```go
// log/slog/doc.go:6-10
/*
Package slog provides structured logging,
in which log records include a message,
a severity level, and various other attributes
expressed as key-value pairs.
*/
```
---
## 2. Section Headers in Package Docs
**Pattern name:** `# Heading` in Doc Comments
**Source citation:** `os/file.go` lines 37–43, `net/http/doc.go` (multiple sections)
**What it does:** Uses `# Section Name` within the package doc comment to organize
long documentation into navigable sections.
**Why:** Large packages need structure. Section headers render as links in pkg.go.dev
and provide a scannable table of contents.
**Anti-pattern:** Wall-of-text package docs; using `===` or `---` (not recognized);
too many sections (fragmenting simple docs).
**Code example from source:**
```go
// os/file.go:37
// # Concurrency
//
// The methods of [File] correspond to file system operations. All are
// safe for concurrent use.
```
---
## 3. Type/Function Comment Convention
**Pattern name:** `// TypeName verb...` or `// FuncName verb...`
**Source citation:** `net/http/server.go` lines 64–82 (Handler), `bufio/scan.go` lines 14–27 (Scanner)
**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.
**Why:** `go doc` extracts the first sentence as a summary. Starting with the name
ensures it reads correctly in both isolation (summary lists) and full context.
This is enforced by convention and checked by linters.
**Anti-pattern:** Starting with "This function..." or "The Foo type..."; starting
with articles ("A Handler is...") for functions (acceptable for types); omitting
the comment entirely.
**Code examples from source:**
```go
// net/http/server.go:64
// A Handler responds to an HTTP request.
// bufio/scan.go:14-17
// Scanner provides a convenient interface for reading data such as
// a file of newline-delimited lines of text.
// net/http/request.go:867
// NewRequest wraps NewRequestWithContext using context.Background.
// os/file.go:389-390
// Open opens the named file for reading.
// regexp/regexp.go:310-312
// MustCompile is like [Compile] but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
```
---
## 4. Doc Links (Square Bracket References)
**Pattern name:** `[TypeName]`, `[Package.Symbol]`, `[Method]` Links
**Source citation:** `net/http/server.go` lines 65–70, `os/file.go` line 9
**What it does:** Doc comments use `[SymbolName]` to create hyperlinks to other
identifiers. These render as clickable links on pkg.go.dev.
**Why:** Cross-references help users navigate the API. Links are concise and
don't clutter the plain-text rendering.
**Anti-pattern:** Using full URLs to godoc pages; not linking related types;
over-linking (every mention of every type).
**Code examples from source:**
```go
// net/http/server.go:65-70
// [Handler.ServeHTTP] should write reply headers and data to the [ResponseWriter]
// and then return. Returning signals that the request is finished; it
// is not valid to use the [ResponseWriter] or read from the
// [Request.Body] after or concurrently with the completion of the
// ServeHTTP call.
// os/file.go:9-11
// if a call that takes a file name fails, such as [Open] or [Stat], the error
// will include the failing file name when printed and will be of type
// [*PathError], which may be unpacked for more information.
```
---
## 5. Example Test Functions
**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
**What it does:** Functions named `Example`, `ExampleXxx`, or `ExampleType_Method`
in `_test.go` files serve as both executable tests and documentation. They include
an `// Output:` comment that `go test` verifies.
**Why:** Examples that compile, run, and are verified can never go stale. They appear
in `go doc` and pkg.go.dev alongside the relevant symbol. They teach by showing
real, working code.
**Anti-pattern:** Examples that don't compile; examples without Output comments
(not verified); examples in README that drift from reality.
**Code examples from source:**
```go
// regexp/example_test.go:13-28
func Example() {
// Compile the expression once, usually at init time.
// Use raw strings to avoid having to quote the backslashes.
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)
fmt.Println(validID.MatchString("adam[23]"))
fmt.Println(validID.MatchString("eve[7]"))
fmt.Println(validID.MatchString("Job[48]"))
fmt.Println(validID.MatchString("snakey"))
// Output:
// true
// true
// false
// false
}
```
```go
// net/http/example_handle_test.go:16-31
type countHandler struct {
mu sync.Mutex // guards n
n int
}
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
h.n++
fmt.Fprintf(w, "count is %d\n", h.n)
}
func ExampleHandle() {
http.Handle("/count", new(countHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
---
## 6. Inline Code Examples in Doc Comments
**Pattern name:** Indented Code Blocks in Comments
**Source citation:** `os/file.go` lines 17–35, `time/time.go` lines 928–933
**What it does:** Doc comments include indented code snippets (4 spaces) that render
as preformatted code blocks in godoc.
**Why:** Shows typical usage patterns directly in the doc comment without requiring
a separate Example test function. Good for short, illustrative snippets.
**Anti-pattern:** Non-indented code that doesn't render as code; examples too long
for inline (use Example functions instead); examples that reference unexported symbols.
**Code examples from source:**
```go
// os/file.go:17-21
// Here is a simple example, opening a file and reading some of it.
//
// file, err := os.Open("file.go") // For read access.
// if err != nil {
// log.Fatal(err)
// }
// time/time.go:928-933
// To count the number of units in a [Duration], divide:
//
// second := time.Second
// fmt.Print(int64(second/time.Millisecond)) // prints 1000
//
// To convert an integer number of units to a Duration, multiply:
//
// seconds := 10
// fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
```
---
## 7. Deprecated Annotations
**Pattern name:** `// Deprecated: ...` in Doc Comments
**Source citation:** `net/http/server.go` line 57 (ErrWriteAfterFlush), `os/file.go` lines 93–95
**What it does:** A paragraph starting with `Deprecated:` marks an identifier as
deprecated and explains what to use instead.
**Why:** Recognized by tooling (go vet, staticcheck, IDEs). Provides a migration
path without breaking backward compatibility.
**Anti-pattern:** Removing deprecated APIs (breaks semver); deprecating without
suggesting an alternative; using non-standard deprecation markers.
**Code example from source:**
```go
// net/http/server.go:55-57
// Deprecated: ErrWriteAfterFlush is no longer returned by
// anything in the net/http package. Callers should not
// compare errors against this variable.
ErrWriteAfterFlush = errors.New("unused")
```
---
## 8. Error Documentation Convention
**Pattern name:** "If there is an error, it will be of type [*XxxError]"
**Source citation:** `os/file.go` lines 388, 406
**What it does:** Functions document the concrete error type they return, enabling
callers to type-assert for additional context.
**Why:** Go's error handling relies on type assertions and `errors.Is/As`. Knowing
the concrete type lets callers extract structured information (path, operation,
underlying cause).
**Anti-pattern:** Returning opaque errors with no documented structure; returning
different error types from the same function without documenting which.
**Code example from source:**
```go
// os/file.go:388-390
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode [O_RDONLY].
// If there is an error, it will be of type [*PathError].
func Open(name string) (*File, error) {
```
---
## 9. Concurrency Documentation
**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
**What it does:** Doc comments explicitly state the concurrency safety of a type
or note exceptions where concurrent use is not safe.
**Why:** Go programs are inherently concurrent. Without explicit documentation,
users must guess whether a type needs external synchronization.
**Anti-pattern:** Leaving concurrency safety undocumented; documenting it
inconsistently across methods; saying "thread-safe" (Java-ism, use "safe for
concurrent use by multiple goroutines").
**Code examples from source:**
```go
// net/http/transport.go:79-80
// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.
// os/types.go:17
// The methods of File are safe for concurrent use.
// regexp/regexp.go:77-79
// A Regexp is safe for concurrent use by multiple goroutines,
// except for configuration methods, such as [Regexp.Longest].
```
+29 -13
View File
@@ -300,11 +300,17 @@ func Join(errs ...error) error {
}
```
The `joinError` type implements `Unwrap() []error`, making both `Is` and `As` traverse correctly.
The `joinError` type implements `Unwrap() []error`:
```go
// src/errors/join.go:57 (implicit from structure)
func (e *joinError) Unwrap() []error {
return e.errs
}
```
### Why
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error.
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error. Both `Is` and `As` traverse the tree correctly.
```go
var errs []error
@@ -430,7 +436,8 @@ func (e *MyError) Error() string { return e.Err.Error() }
```go
// src/errors/errors.go:76-83
// ErrUnsupported indicates that a requested operation cannot be performed,
// because it is unsupported.
// because it is unsupported. For example, a call to os.Link when using a
// file system that does not support hard links.
//
// Functions and methods should not return this error but should instead
// return an error including appropriate context that satisfies
@@ -449,6 +456,8 @@ This pattern separates "what happened" (detailed context) from "what kind of fai
return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported)
```
Callers check the sentinel; the message provides context.
### Anti-pattern
```go
@@ -458,39 +467,46 @@ return errors.ErrUnsupported // no info about what operation or why
---
## 10. Error String Conventions
## 10. Error Value Patterns in net/http
### Source: `src/net/http/server.go:39-56`
```go
// src/net/http/server.go:39-56
var (
ErrBodyNotAllowed = internal.ErrBodyNotAllowed
ErrHijacked = errors.New("http: connection has been hijacked")
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
ErrWriteAfterFlush = errors.New("unused") // Deprecated
)
```
### Why
- Errors are package-level `var` (not `const`) — they're pointer values
- Error strings start with the package name (`"http: ..."`) for disambiguation in logs
- Deprecated errors are kept for backward compatibility but marked clearly
- Internal errors can be aliased (`ErrBodyNotAllowed = internal.ErrBodyNotAllowed`) to share across internal packages
### Convention: Error String Format
```
package: description
```
- Lowercase (no capital first letter)
- No trailing punctuation
- Package prefix for disambiguation
Examples from stdlib:
- `"http: connection has been hijacked"`
- `"sql: unknown driver %q (forgotten import?)"`
- `"json: unsupported type: %s"`
### Anti-pattern
```go
// DON'T: Capitalize error strings
errors.New("Connection has been hijacked")
errors.New("Connection has been hijacked") // Go convention: lowercase
// DON'T: End with punctuation
errors.New("connection failed.")
// DON'T: Include redundant "error" word
errors.New("http error: connection failed") // it's already an error
// DON'T: End error strings with punctuation
errors.New("connection failed.") // no trailing period
```
---
+174 -90
View File
@@ -43,6 +43,7 @@ The package comment:
### Convention
- First sentence: `"Package X does Y."` or `"Package X provides Y."`
- Subsequent paragraphs: contracts, caveats, links to deeper docs
- For multi-file packages, put the package comment in `doc.go` or the primary file
### Anti-pattern
@@ -54,6 +55,10 @@ package myutil
// DON'T: Restate the obvious
// Package http provides HTTP stuff.
package http
// DON'T: Put implementation details in the package comment
// Package auth uses bcrypt with cost 12 and stores hashes in PostgreSQL.
package auth
```
---
@@ -69,10 +74,16 @@ package http
- `net/http` — not `net/httpserver`
- `encoding/json` — not `encoding/jsonparser`
- `context` — not `ctx` or `contexts`
- `errors` — not `errs` or `errorhandling`
### Why
Go package names are **short, lowercase, no underscores or mixedCaps**. The package name is part of every qualified identifier:
Go package names are:
- **Short** — one word, lowercase, no underscores or mixedCaps
- **Clear** — the name is the context for everything inside it
- **Singular** (usually) — `context` not `contexts`, `error` exception (`errors` has functions)
The package name is part of every qualified identifier: `http.Handler`, `json.Marshal`, `context.Context`. Redundancy in naming is wasted keystrokes:
```go
// Good: package name provides context
@@ -84,14 +95,18 @@ context.Context // the type IS the context
### Anti-pattern
```go
// DON'T: Stutter
// DON'T: Stutter (repeat package name in exported identifiers)
package http
type HTTPServer struct{} // http.HTTPServer — redundant
func NewHTTPClient() // http.NewHTTPClient — say "http" twice
// DON'T: Utility package names
// DON'T: Use utility/helper package names
package utils // what does it DO?
package helpers // grab bag, no cohesion
package common // everything ends up here
// DON'T: Use plural when singular works
package requests // should be: package request
```
---
@@ -104,45 +119,64 @@ package common // everything ends up here
src/net/http/internal/
├── ascii/
├── chunked.go
├── common.go
├── http2/
├── httpcommon/
├── httpsfv/
├── sniff.go
└── testcert/
```
### Why
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. This lets you share code between sub-packages without making it public API.
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. For example:
- `net/http/internal/ascii` can be imported by `net/http` and `net/http/...`
- It **cannot** be imported by `net/url` or any other package
- `net/http/internal/ascii` → importable by `net/http` and children
- NOT importable by `net/url` or any other package
This lets you share code between sub-packages without making it part of the public API.
### Usage Guidelines
```
myproject/
├── internal/ # shared across the project, but not importable externally
│ ├── auth/
│ └── metrics/
├── cmd/
│ └── server/
└── pkg/ # actually public API (if you use this convention)
└── client/
```
### Anti-pattern
```go
// DON'T: Export implementation details
// DON'T: Export implementation details that should be internal
package mylib
func HelperThatOnlyIUse() {} // pollutes API surface
// DO: Move to internal/
// DON'T: Put everything in internal/ (nothing is reusable)
// Balance: internal/ for implementation; exported packages for contracts
```
---
## 4. Export Rules — The Capital Letter Boundary
### Source: `src/io/io.go` — exported vs unexported
### Source: Throughout stdlib — the convention is the language itself
```go
// src/io/io.go
var EOF = errors.New("EOF") // exported: uppercase
var errInvalidWrite = errors.New(...) // unexported: lowercase
// src/io/io.go:622-625
type teeReader struct { // unexported type
r Reader
w Writer
}
// src/io/io.go:618
func TeeReader(r Reader, w Writer) Reader { // exported constructor
return &teeReader{r, w}
}
@@ -150,27 +184,42 @@ func TeeReader(r Reader, w Writer) Reader { // exported constructor
### Why
`teeReader` is unexported because:
The exported/unexported boundary is Go's encapsulation mechanism. `teeReader` is unexported because:
1. Users don't need to know its implementation
2. The return type is `Reader` (interface) — maximum flexibility
2. The return type is `Reader` (the interface) — maximum flexibility
3. The struct's fields can change without breaking anyone
### Pattern: Exported Function, Unexported Type
```go
// Export the constructor, not the type
func NewParser(r io.Reader) *parser { ... } // WRONG: can't return unexported type
// Correct: return via interface or exported type
func TeeReader(r Reader, w Writer) Reader { return &teeReader{r, w} }
```
### Anti-pattern
```go
// DON'T: Export everything "just in case"
type Parser struct {
Input string // should this be settable?
buffer []byte // internal state
Input string // should this be settable? probably not
buffer []byte // internal state — definitely not
pos int
}
// DON'T: Make internal state accessible
type DB struct {
Pool []*Conn // callers shouldn't manipulate the pool directly
}
```
---
## 5. init() Functions — Use Sparingly
### Source: `src/net/http/http2.go:37`
### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31`
```go
// src/net/http/http2.go:37
@@ -181,15 +230,16 @@ func init() {
### Why
The stdlib uses `init()` for:
`init()` runs automatically at program start, in dependency order. The stdlib uses it for:
- **Driver registration** (database drivers register via init)
- **Protocol negotiation** (HTTP/2 registers its handler)
- **Configuration from build tags** (`servemux121.go` — compatibility shim)
### Rules
1. Should have no side effects beyond registration
2. No errors possible (can't return error from init)
3. Keep them short
1. `init()` should have no side effects beyond registration
2. No errors should be possible (can't return error from init)
3. Keep them short — they block program startup
4. Prefer explicit initialization in `main()` when possible
### Anti-pattern
@@ -201,11 +251,16 @@ func init() {
cache = loadGigabyteFile() // blocks startup
}
// DO: Prefer explicit setup in main()
// DON'T: Use init for configuration
func init() {
port = os.Getenv("PORT") // harder to test, implicit dependency
}
// DO: Prefer explicit setup
func main() {
db, err := connectToDatabase()
if err != nil {
log.Fatal(err)
log.Fatal(err) // clear failure point
}
}
```
@@ -214,10 +269,12 @@ func main() {
## 6. Functional Options Pattern
The stdlib uses struct-based configuration (`http.Server`, `tls.Config`). The functional options pattern emerged from the community for APIs with many optional parameters:
### Source: Not directly in stdlib, but `net/http.Server` and `database/sql.DB` demonstrate the problem it solves
The stdlib uses struct-based configuration (Server, Transport, DB config via setters). The functional options pattern emerged from the community to solve the "many optional parameters" problem:
```go
// The pattern (idiom from Rob Pike/Dave Cheney):
// The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney):
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
@@ -226,6 +283,12 @@ func WithTimeout(d time.Duration) Option {
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
@@ -235,10 +298,10 @@ func NewServer(addr string, opts ...Option) *Server {
}
```
### What stdlib uses: Config structs
### What the stdlib uses instead: Config structs
```go
// net/http — struct literal configuration
// src/net/http/server.go (Server struct acts as config)
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
@@ -251,8 +314,20 @@ srv := &http.Server{
| Approach | When |
|----------|------|
| Config struct | Few options, all data (stdlib preference) |
| Functional options | Many options, some involve behavior, public API stability |
| Config struct | Few options, all are data (stdlib preference) |
| Functional options | Many options, some involve behavior, public API stability matters |
| Builder pattern | Rare in Go — usually overkill |
### Anti-pattern
```go
// DON'T: Long parameter lists
func NewServer(addr string, timeout time.Duration, maxConns int,
logger *log.Logger, tls *tls.Config, handler Handler) *Server
// DON'T: Use functional options when a simple struct suffices
// (Over-engineering for 2-3 fields)
```
---
@@ -272,6 +347,7 @@ func OpenDB(c driver.Connector) *DB {
db := &DB{
connector: c,
openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string),
stop: cancel,
}
go db.connectionOpener(ctx)
@@ -281,19 +357,24 @@ func OpenDB(c driver.Connector) *DB {
### Why
- `NewX()` when construction is trivial
- `OpenX()` when construction involves resources or can fail
- Return `*T` (concrete), not an interface
- Zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`)
- `NewX()` when construction is trivial (just allocate)
- `OpenX()` or `NewXWithConfig()` when construction involves resources, validation, or can fail
- Return `*T` (pointer to concrete type), not an interface
The zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`), making constructors unnecessary.
### Anti-pattern
```go
// DON'T: Constructor that returns interface
func NewWriter() io.Writer { return &myWriter{} } // hides methods
// DON'T: Constructor that returns interface (hides useful methods)
func NewWriter() io.Writer { return &myWriter{} }
// DON'T: Require constructor when zero value works
// var b bytes.Buffer ← just works
type Buffer struct {
buf []byte
// ...
}
// var b bytes.Buffer ← just works, no New needed
```
---
@@ -309,42 +390,54 @@ src/
├── net/ # network primitives
│ ├── http/ # HTTP protocol
│ └── url/ # URL parsing
├── encoding/
├── encoding/ # encoding interfaces
│ ├── json/ # JSON codec
│ └── xml/ # XML codec
├── database/
│ └── sql/ # SQL abstraction
│ └── driver/ # SPI for drivers
│ └── sql/ # SQL database abstraction
│ └── driver/ # SPI for database drivers
└── context/ # cancellation propagation
```
### Why
Each package has a single, clear responsibility. Packages communicate through interfaces, not shared state.
Each package has a single, clear responsibility:
- `io` defines interfaces; `os` implements them for files
- `encoding/json` handles JSON; `encoding/xml` handles XML
- `database/sql` is the user-facing API; `database/sql/driver` is the implementor-facing SPI
### Anti-pattern
```go
// DON'T: Package per type (50 packages with 1 file each)
package user
package order
package payment
// DON'T: Package per type
package user // just has User struct
package order // just has Order struct
package payment // just has Payment struct
// 50 packages with 1 file each — Go prefers fewer, larger packages
// DON'T: Circular dependencies
package a imports package b
package b imports package a // compile error
// FIX: Extract shared types into a third package, or merge
```
---
## 9. API Layering — User vs Implementor (database/sql)
## 9. API Design — database/sql Separation of Concerns
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
Two distinct APIs in one subsystem:
**User-facing (database/sql):**
```go
db, _ := sql.Open("postgres", connStr)
rows, _ := db.QueryContext(ctx, "SELECT ...")
defer rows.Close()
for rows.Next() {
rows.Scan(&id, &name)
}
```
**Driver-facing (database/sql/driver):**
@@ -361,7 +454,25 @@ type Conn interface {
### Why
The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic. Clean separation: users get high-level safe API; drivers implement minimal interface.
The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic. Clean separation:
- Users get a high-level, safe API with pooling and retry
- Drivers implement a low-level, minimal interface
- The `sql` package mediates between them
### Anti-pattern
```go
// DON'T: Expose implementation to users
type DB struct {
driver driver.Conn // users shouldn't touch this
}
// DON'T: Mix user and implementor APIs in one interface
type Database interface {
Query(sql string) Rows // user method
Open(dsn string) Conn // driver method — different audiences
}
```
---
@@ -370,10 +481,14 @@ The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic.
### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252`
```go
// src/context/context.go:132-164 (from doc)
// src/context/context.go:132-164 (from doc comment)
// Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// type key int
//
// var userKey key
//
// func NewContext(ctx context.Context, u *User) context.Context {
@@ -400,68 +515,37 @@ type contextKey struct {
### Why
- **Unexported key type** prevents other packages from accessing your values
- **Type-safe accessors** avoid repeated type assertions
- **Pointer-based keys** guarantee uniqueness
- **Unexported key type** prevents other packages from accessing or overwriting your values
- **Type-safe accessors** (`FromContext`) avoid type assertions at every call site
- **Pointer-based keys** (`&contextKey{...}`) guarantee uniqueness even with same string names
### Anti-pattern
```go
// DON'T: Use string keys (collision risk)
// DON'T: Use string keys (any package can collide)
ctx = context.WithValue(ctx, "user", user)
// DON'T: Use exported key types (anyone can access)
type Key string
const UserKey Key = "user" // other packages can use this key
// DON'T: Store optional parameters in context
ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
```
---
## 11. Struct Tags for Codec Configuration
### Source: `src/encoding/json/tags.go:17-21`, `src/encoding/json/encode.go:101-181`
```go
// src/encoding/json/tags.go:17-21
func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}
```
Usage in struct definitions:
```go
type Person struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Secret string `json:"-"` // always omitted
Address string `json:"addr,omitempty"`
}
```
### Why
Struct tags are metadata for codecs. The `json` package reads `json:"..."` tags via reflection to control field names and behavior. The format is `key:"value"` with comma-separated options.
### Convention (from encode.go docs, line 101-181)
- `json:"fieldname"` — override JSON key name
- `json:",omitempty"` — omit if zero value
- `json:"-"` — never include
- `json:"-,"` — use literal `-` as name
---
## Summary: Package Design Principles
| Principle | Rule |
|-----------|------|
| Package comment | `"Package X does Y."` before `package` keyword |
| Naming | Short, lowercase, no stutter |
| Encapsulation | `internal/` for private shared code |
| Exports | Minimum surface; unexported by default |
| init() | Only for registration; prefer explicit setup |
| Naming | Short, lowercase, no stutter (`http.Server` not `http.HTTPServer`) |
| Encapsulation | `internal/` for shared-but-private code |
| Exports | Minimum viable surface; unexported by default |
| init() | Only for registration; keep trivial |
| Constructors | `NewX()` → `*T`; prefer usable zero values |
| Organization | One concern per package |
| API layers | Separate user from implementor (SPI) |
| Organization | One concern per package; no circular deps |
| API layers | Separate user-facing from implementor-facing (SPI) |
| Context values | Unexported key type + typed accessors |
| Configuration | Struct literals or functional options |
| Configuration | Struct literals (stdlib) or functional options (community) |
-404
View File
@@ -1,404 +0,0 @@
# Struct Design Patterns in the Go Standard Library
## 1. Zero-Value Usability
**Pattern name:** Zero Value Ready
**Source citation:** `net/http/client.go` lines 31–35, `strings/builder.go` lines 14–16
**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.
**Why:** Eliminates mandatory constructors, reduces boilerplate, makes the type
self-documenting about its defaults. Users can write `var c http.Client` and start
making requests.
**Anti-pattern:** Requiring a constructor for basic use; panicking on zero-value use;
requiring all fields be set before the type is functional.
**Code examples from source:**
```go
// net/http/client.go:31-35
// A Client is an HTTP client. Its zero value ([DefaultClient]) is a
// usable client that uses [DefaultTransport].
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
// net/http/client.go:109
var DefaultClient = &Client{}
```
```go
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
```
```go
// bytes/buffer.go:19-20
// A Buffer is a variable-sized buffer of bytes with [Buffer.Read] and [Buffer.Write] methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
buf []byte
off int
lastRead readOp
}
```
---
## 2. Unexported Struct with Exported Wrapper
**Pattern name:** Indirection via Unexported Impl
**Source citation:** `os/types.go` lines 16–20, `os/file_unix.go` lines 59–71
**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
exported wrapper.
**Why:** Prevents users from directly constructing or copying the implementation struct.
Allows platform-specific implementations behind a uniform exported API. The extra
indirection ensures finalizers close the correct descriptor.
**Anti-pattern:** Exporting all implementation fields; allowing users to construct
the struct via a literal (bypassing invariants); needing platform #ifdefs in the
public API.
**Code example from source:**
```go
// os/types.go:16-20
// File represents an open file descriptor.
//
// The methods of File are safe for concurrent use.
type File struct {
*file // os specific
}
// os/file_unix.go:59-71
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
pfd poll.FD
name string
dirinfo atomic.Pointer[dirInfo]
nonblock bool
stdoutOrErr bool
appendMode bool
inRoot bool
}
```
---
## 3. Constructor Functions (NewXxx)
**Pattern name:** NewXxx Constructor
**Source citation:** `bufio/scan.go` lines 89–96, `bufio/bufio.go` lines 50–60
**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
value alone.
**Why:** When a type has mandatory dependencies (e.g., an `io.Reader`), a constructor
clearly communicates what's required. The constructor can set internal invariants
(buffer sizes, split functions) that users shouldn't need to know about.
**Anti-pattern:** Forcing users to manually set unexported fields; having a constructor
that takes 10 optional parameters (use config struct instead); requiring New when
zero value would suffice.
**Code examples from source:**
```go
// bufio/scan.go:89-96
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
```
```go
// bufio/bufio.go:50-62
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
r := new(Reader)
r.reset(make([]byte, max(size, minReadBufferSize)), rd)
return r
}
// NewReader returns a new [Reader] whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
```
```go
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
```
---
## 4. NewXxx with Size/Options Variant
**Pattern name:** NewXxx / NewXxxSize Pair
**Source citation:** `bufio/bufio.go` lines 50, 62, 589, 607
**What it does:** Provides two constructors — one with defaults (`NewReader`) and one
with explicit configuration (`NewReaderSize`). The default version calls the
configurable one.
**Why:** Most users want the default; power users need control. Layering avoids a
proliferation of constructor parameters for the common case.
**Anti-pattern:** Having only the complex constructor; making users guess the right
buffer size; inconsistent naming (e.g., `NewReaderWithSize`).
**Code example from source:**
```go
// bufio/bufio.go:589-607
func NewWriterSize(w io.Writer, size int) *Writer {
// ...
}
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
```
---
## 5. Config Struct Pattern
**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
**What it does:** A struct with exported, documented fields provides all
configuration knobs. Nil/zero values always mean "use the default".
**Why:** Self-documenting via godoc; no need for a setter method per option; easy to
construct partially; serializable; the zero value works. This is Go's primary
configuration pattern (preferred over functional options in the stdlib).
**Anti-pattern:** Undocumented fields; requiring all fields set; using sentinel values
other than zero/nil for defaults; providing setters when direct assignment works.
**Code example from source:**
```go
// net/http/server.go:3020-3075 (abbreviated)
type Server struct {
Addr string // ":http" if empty
Handler Handler // http.DefaultServeMux if nil
TLSConfig *tls.Config // optional
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxHeaderBytes int // DefaultMaxHeaderBytes if zero
ErrorLog *log.Logger // log.Default() if nil
// ...
}
```
```go
// log/slog/handler.go:135-175
type HandlerOptions struct {
AddSource bool
Level Leveler // LevelInfo if nil
ReplaceAttr func(groups []string, a Attr) Attr
}
// Usage: If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
// ...
}
```
---
## 6. Interface-Based Pluggability
**Pattern name:** Interface Abstraction for Pluggable Implementations
**Source citation:** `crypto/crypto.go` lines 180–200, `net/http/transport.go` lines 66–82
**What it does:** Core behavior is defined via an interface. The package provides
a default concrete implementation, but any user type satisfying the interface
can be substituted.
**Why:** Decouples high-level logic from low-level implementation. Enables testing
(mock transports), hardware integration (HSM-backed signers), and third-party
extensions without forking the package.
**Anti-pattern:** Concrete-type coupling everywhere; interfaces with too many methods
(hard to implement); accepting an interface but only ever using one implementation.
**Code example from source:**
```go
// crypto/crypto.go:180-200
// Signer is an interface for an opaque private key that can be used for
// signing operations. For example, an RSA key kept in a hardware module.
type Signer interface {
Public() PublicKey
Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
```
```go
// net/http/transport.go (line 66+)
// Transport is an implementation of [RoundTripper] that supports HTTP,
// HTTPS, and HTTP proxies...
// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.
// net/http/client.go:59-60
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
```
---
## 7. Copy Protection via Dynamic Check
**Pattern name:** copyCheck (Runtime Copy Detection)
**Source citation:** `strings/builder.go` lines 25–40
**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
differ, the struct was copied — it panics.
**Why:** Go has no language-level move semantics. For types where copying after first
use would cause data corruption or unsafe behavior (e.g., sharing an unsafe string
buffer), a runtime check is the pragmatic solution.
**Anti-pattern:** Silently allowing copies that corrupt state; using `sync.Mutex`-style
`noCopy` (vet catches it but it doesn't work for zero vs non-zero discrimination).
**Code example from source:**
```go
// strings/builder.go:25-40
func (b *Builder) copyCheck() {
if b.addr == nil {
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
```
---
## 8. DefaultXxx Singleton
**Pattern name:** Package-Level Default Instance
**Source citation:** `net/http/client.go` line 109, `net/http/transport.go` lines 47–58
**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.
**Why:** Makes the simple case trivial (`http.Get(url)`) while allowing custom
instances for advanced use. Users never need to touch the defaults unless they
have specific requirements.
**Anti-pattern:** Forcing construction for basic use; not providing convenience
functions; making the default mutable in ways that affect all users.
**Code example from source:**
```go
// net/http/client.go:108-109
// DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
var DefaultClient = &Client{}
// net/http/transport.go:47-58
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,
}
```
---
## 9. Functional Configuration via Method Chaining (Scanner Pattern)
**Pattern name:** Post-Construction Configuration via Methods
**Source citation:** `bufio/scan.go` lines 275–293
**What it does:** After construction with `NewScanner`, optional configuration is
applied via methods (`Split`, `Buffer`) before the first call to `Scan`.
**Why:** Keeps the constructor minimal (only the required `io.Reader`). Optional
configuration is discoverable via methods. Panics if called after scanning starts
(enforcing a construction → configure → use lifecycle).
**Anti-pattern:** Trying to pass all options into the constructor; allowing
configuration changes mid-use that corrupt state.
**Code example from source:**
```go
// bufio/scan.go:275-293
// Buffer sets the initial buffer to use when scanning
// and the maximum size of buffer that may be allocated during scanning.
// ...
// Buffer panics if it is called after scanning has started.
func (s *Scanner) Buffer(buf []byte, max int) {
if s.scanCalled {
panic("Buffer called after Scan")
}
s.buf = buf
s.maxTokenSize = max
}
// Split sets the split function for the [Scanner].
// ...
// Split panics if it is called after scanning has started.
func (s *Scanner) Split(split SplitFunc) {
if s.scanCalled {
panic("Split called after Scan")
}
s.split = split
}
```
-464
View File
@@ -1,464 +0,0 @@
# Code Style Patterns in the Go Standard Library
## 1. Naming Conventions: mixedCaps (No Underscores)
**Pattern name:** mixedCaps / MixedCaps
**Source citation:** All stdlib code (enforced by `gofmt` convention, documented in Effective Go)
**What it does:** All identifiers use mixedCaps (unexported) or MixedCaps (exported).
Underscores are never used in Go names except for test helpers and generated code.
**Why:** Consistent casing makes code scannable. The exported/unexported distinction
is communicated solely through initial capitalization — no separate `public`/`private`
keywords needed.
**Anti-pattern:** `snake_case` names; `ALL_CAPS` for constants; Hungarian notation
(`strName`, `iCount`).
**Code examples from source:**
```go
// net/http/server.go — exported
type Server struct { ... }
func ListenAndServe(addr string, handler Handler) error
// net/http/server.go — unexported
func (s *Server) shuttingDown() bool
const shutdownPollIntervalMax = 500 * time.Millisecond
```
---
## 2. Acronyms Are All-Caps
**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`)
**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.
**Why:** Consistency. `URL` not `Url`, `ID` not `Id`, `HTTP` not `Http`. This
applies even mid-word: `ServeHTTP`, `xmlEncoder`, `htmlEscape`.
**Anti-pattern:** `Url`, `Http`, `Json`, `Id` — mixing cases within an acronym.
**Code examples from source:**
```go
// net/http/request.go:130
URL *url.URL
// net/http/request.go:822
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
// net/http/server.go:3041
TLSConfig *tls.Config
// encoding/json/stream.go:280
var _ Marshaler = (*RawMessage)(nil)
```
---
## 3. File Organization by Responsibility
**Pattern name:** One Concept Per File
**Source citation:** `net/http/` directory structure
**What it does:** Large packages split code into files by topic/type: `client.go`,
`server.go`, `transport.go`, `request.go`, `response.go`, `cookie.go`, `header.go`,
`fs.go`, `doc.go`. Each file is focused.
**Why:** Navigability. When you want to find client logic, you open `client.go`.
Files stay manageable sizes. Related code lives together.
**Anti-pattern:** One giant file with everything; splitting by access level
(`public.go` / `private.go`); splitting by method count rather than concept.
**File layout from `net/http/`:**
```
client.go — Client type and methods
transport.go — Transport type (low-level RoundTripper)
server.go — Server, Handler, ServeMux
request.go — Request type and parsing
response.go — Response type and reading
cookie.go — Cookie parsing and serialization
header.go — Header type and canonicalization
fs.go — FileServer, file serving
doc.go — Package documentation
clone.go — Clone helpers
method.go — HTTP method constants
pattern.go — URL pattern matching (ServeMux routing)
```
---
## 4. Blank Identifier for Interface Compliance
**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
**What it does:** A package-level `var _ InterfaceName = (*ConcreteType)(nil)` declares
that the concrete type must satisfy the interface. The compiler verifies this at
build time.
**Why:** Catches interface drift at compile time without creating an instance. The
blank identifier discards the value — this is purely a static assertion.
**Anti-pattern:** Relying on tests to catch interface conformance; skipping the check
and discovering the mismatch at runtime; using reflection.
**Code examples from source:**
```go
// io/io.go:645
var _ ReaderFrom = discard{}
// os/file.go:747-750
var _ fs.StatFS = dirFS("")
var _ fs.ReadFileFS = dirFS("")
var _ fs.ReadDirFS = dirFS("")
var _ fs.ReadLinkFS = dirFS("")
// encoding/json/stream.go:280-281
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)
// net/http/server.go:4071
var _ Pusher = (*timeoutWriter)(nil)
```
---
## 5. Named Return Values
**Pattern name:** Named Returns for Documentation (and Defer)
**Source citation:** `io/io.go` lines 87, 100, 314, 387; `os/file.go` lines 140, 175
**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.
**Why:** `(n int, err error)` is immediately understandable — `n` is the byte count.
Named returns also enable `defer func() { err = wrap(err) }()` patterns.
**Anti-pattern:** Naming returns for trivial functions where the types are
self-explanatory; using named returns as implicit variables throughout the function
body (confusing naked returns); always using naked `return` statements.
**Code examples from source:**
```go
// io/io.go:87 — Interface documentation
type Reader interface {
Read(p []byte) (n int, err error)
}
// io/io.go:100
type Writer interface {
Write(p []byte) (n int, err error)
}
// io/io.go:387 — Named return used with defer-style logic
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}
// os/file.go:140 — Named return for readability
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}
```
---
## 6. Defer for Resource Cleanup
**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
**What it does:** Resources acquired at the top of a scope are immediately deferred
for cleanup. Mutexes are locked then immediately `defer Unlock()`'d.
**Why:** Guarantees cleanup regardless of return path (early returns, panics). Keeps
the acquire/release pair visually adjacent. Reduces bugs from forgotten unlocks.
**Anti-pattern:** Manual unlock at each return point; deferring in a loop (deferred
calls accumulate until function exit); deferring expensive operations that should
run earlier.
**Code examples from source:**
```go
// net/http/server.go:3173-3174
func (s *Server) Close() error {
s.inShutdown.Store(true)
s.mu.Lock()
defer s.mu.Unlock()
// ...
}
// net/http/example_handle_test.go:21-22
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
h.n++
fmt.Fprintf(w, "count is %d\n", h.n)
}
```
---
## 7. Error Wrapping and Sentinel Errors
**Pattern name:** Sentinel Errors + Structured Error Types
**Source citation:** `os/error.go` lines 14–27, `os/error.go` lines 46–67
**What it does:** Package-level sentinel errors (`ErrNotExist`, `ErrPermission`) are
declared as `var` for use with `errors.Is()`. Structured error types (`*PathError`,
`*SyscallError`) carry context and implement `Unwrap()` for the errors chain.
**Why:** Enables programmatic error handling without string matching. `errors.Is(err, os.ErrNotExist)` works regardless of wrapping depth. Structured types let callers
extract the operation, path, or underlying syscall error.
**Anti-pattern:** Comparing error strings; creating unique error types for every
possible failure; not implementing `Unwrap`; sentinel errors as `const` (breaks
`errors.Is` for wrapped errors — use `var`).
**Code examples from source:**
```go
// os/error.go:14-27
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
)
// os/error.go:46
type PathError = fs.PathError
// os/error.go:49-57
type SyscallError struct {
Syscall string
Err error
}
func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }
func (e *SyscallError) Unwrap() error { return e.Err }
```
---
## 8. Receiver Naming: Short, Consistent, Never `this`/`self`
**Pattern name:** Single-Letter or Short Receiver Names
**Source citation:** All stdlib code; `net/http/server.go` uses `s` for Server, `bufio/scan.go` uses `s` for Scanner
**What it does:** Method receivers use 1–2 letter abbreviations of the type name,
consistent across all methods of that type: `s` for `*Server`, `b` for `*Builder`,
`f` for `*File`, `t` for `*Timer`.
**Why:** Receivers appear on every method. Short names reduce visual noise. Consistency
within a type avoids confusion. `this`/`self` are alien to Go's conventions.
**Anti-pattern:** `this`, `self`, `me`; long receiver names like `server`, `scanner`;
inconsistent receivers across methods of the same type.
**Code examples from source:**
```go
// net/http/server.go
func (s *Server) ListenAndServe() error { ... }
func (s *Server) Serve(l net.Listener) error { ... }
func (s *Server) Shutdown(ctx context.Context) error { ... }
// strings/builder.go
func (b *Builder) WriteString(s string) (int, error) { ... }
func (b *Builder) String() string { ... }
func (b *Builder) Grow(n int) { ... }
// os/file.go
func (f *File) Read(b []byte) (n int, err error) { ... }
func (f *File) Name() string { ... }
```
---
## 9. Constants: Typed, Grouped, with iota
**Pattern name:** Typed Constants with iota
**Source citation:** `crypto/crypto.go` lines 70–85, `time/time.go` lines 936–943
**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
are exhaustively listed together.
**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.
**Anti-pattern:** Untyped numeric constants; separate `const` declarations for related
values; using raw integers in function signatures.
**Code examples from source:**
```go
// crypto/crypto.go:70-85
const (
MD4 Hash = 1 + iota
MD5
SHA1
SHA224
SHA256
// ...
)
// time/time.go:936-943
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
```
---
## 10. Comments: Guard Clauses Over Conditions
**Pattern name:** `// guards x` Field Comments
**Source citation:** `net/http/example_handle_test.go` line 16
**What it does:** When a sync primitive (mutex) protects specific fields, a brief
comment documents what it guards: `mu sync.Mutex // guards n`.
**Why:** Concurrency bugs come from unclear ownership. A one-line comment makes the
lock's scope obvious to every reader.
**Anti-pattern:** No documentation of what a lock protects; locks that protect
"everything" (unclear scope); comments that restate the type.
**Code example from source:**
```go
// net/http/example_handle_test.go:16-17
type countHandler struct {
mu sync.Mutex // guards n
n int
}
```
---
## 11. Duration Type Pattern
**Pattern name:** Named Type for Semantic Units
**Source citation:** `time/time.go` lines 915–943
**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
accidental mixing with raw int64 values.
**Why:** Semantic meaning through the type system. You can't accidentally pass
nanoseconds where seconds are expected. Methods provide conversion and formatting.
Constants like `time.Second` make intent clear.
**Anti-pattern:** Using raw `int64` for durations; accepting `int` parameters for
time intervals; mixing units (milliseconds in one place, seconds in another).
**Code example from source:**
```go
// time/time.go:915
type Duration int64
// time/time.go:947-949
func (d Duration) String() string {
var arr [32]byte
n := d.format(&arr)
return string(arr[n:])
}
```
---
## 12. gofmt: Non-Negotiable Formatting
**Pattern name:** Canonical Formatting via gofmt
**Source citation:** Every file in the Go standard library
**What it does:** All Go code is formatted with `gofmt`. Tabs for indentation, spaces
for alignment. No style debates — the tool decides.
**Why:** Eliminates formatting bikesheds. All Go code looks the same regardless of
author. Diffs show only semantic changes, never style changes. Tooling can parse
and emit canonical code.
**Anti-pattern:** Manual formatting; spaces for indentation; custom alignment rules;
checking in code that `gofmt` would modify.
**Key rules enforced by gofmt:**
- Tabs for indentation
- Opening brace on the same line (`if x {`)
- No optional parentheses (`if x`, not `if (x)`)
- Aligned struct field tags
- One blank line between top-level declarations
- No trailing whitespace
---
## 13. Import Organization
**Pattern name:** Grouped Imports (stdlib / external / internal)
**Source citation:** `net/http/server.go` lines 8–36
**What it does:** Imports are organized in groups separated by blank lines:
1. Standard library
2. External packages (golang.org/x, third-party)
3. Internal packages
The `goimports` tool enforces this automatically.
**Why:** Scannable at a glance. Makes dependency provenance clear (stdlib vs.
external). Reduces merge conflicts.
**Code example from source:**
```go
// net/http/server.go:8-36
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
// ... more stdlib ...
"time"
_ "unsafe" // for linkname
"golang.org/x/net/http/httpguts"
)
```
+670
View File
@@ -0,0 +1,670 @@
# Go Testing Patterns
Patterns extracted from the Go standard library source code.
---
## 1. Table-Driven Tests with Subtests
### Source: `src/encoding/json/encode_test.go:405-430`
```go
// src/encoding/json/encode_test.go:405-430
func TestUnsupportedValues(t *testing.T) {
tests := []struct {
CaseName
in any
}{
{Name(""), math.NaN()},
{Name(""), math.Inf(-1)},
{Name(""), math.Inf(1)},
{Name(""), pointerCycle},
{Name(""), pointerCycleIndirect},
{Name(""), mapCycle},
{Name(""), sliceCycle},
{Name(""), recursiveSliceCycle},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if _, err := Marshal(tt.in); err != nil {
if _, ok := err.(*UnsupportedValueError); !ok {
t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError))
}
} else {
t.Errorf("%s: Marshal error: got nil, want non-nil", tt.Where)
}
})
}
}
```
### Source: `src/encoding/json/encode_test.go:270-328` (with inputs and expected outputs)
```go
// src/encoding/json/encode_test.go:270-328
func TestRoundtripStringTag(t *testing.T) {
tests := []struct {
CaseName
in StringTag
want string
}{{
CaseName: Name("AllTypes"),
in: StringTag{
BoolStr: true,
IntStr: 42,
UintptrStr: 44,
StrStr: "xzbit",
NumberStr: "46",
},
want: `{
"BoolStr": "true",
"IntStr": "42",
...
}`,
}, {
CaseName: Name("StringDoubleEscapes"),
in: StringTag{
StrStr: "\b\f\n\r\t\"\\",
NumberStr: "0",
},
want: `{...}`,
}}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
got, err := MarshalIndent(&tt.in, "", "\t")
if err != nil {
t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err)
}
if got := string(got); got != tt.want {
t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, ...)
}
// Verify round-trip
var s2 StringTag
if err := Unmarshal(got, &s2); err != nil {
t.Fatalf("%s: Decode error: %v", tt.Where, err)
}
if !reflect.DeepEqual(s2, tt.in) {
t.Fatalf("%s: Decode:\n\tinput: %s\n\tgot: %#v\n\twant: %#v", ...)
}
})
}
}
```
### Why
Table-driven tests are Go's signature testing pattern:
1. **All cases visible in one place** — easy to add new cases
2. **t.Run creates subtests** — each case runs independently, can be filtered with `-run`
3. **Uniform structure** — input, expected output, test name
4. **Failures identify which case** — via the case name
### Template
```go
func TestFoo(t *testing.T) {
tests := []struct {
name string
input InputType
want OutputType
wantErr bool
}{
{name: "basic", input: ..., want: ...},
{name: "empty", input: ..., want: ...},
{name: "error case", input: ..., wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Foo(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("Foo() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("Foo() = %v, want %v", got, tt.want)
}
})
}
}
```
### Anti-pattern
```go
// DON'T: Separate test functions for each case
func TestFoo_Basic(t *testing.T) { ... }
func TestFoo_Empty(t *testing.T) { ... }
func TestFoo_Error(t *testing.T) { ... }
// 50 near-identical functions — hard to maintain
// DON'T: Tests without names (hard to identify failures)
tests := []struct{ in, want int }{{1, 2}, {3, 4}}
for _, tt := range tests {
// which test failed? index 0 or 1?
}
```
---
## 2. t.Helper() — Clean Error Reporting
### Source: `src/testing/testing.go:1415-1435`
```go
// src/testing/testing.go:1415-1435
func (c *common) Helper() {
if c.isSynctest {
c = c.parent
}
c.mu.Lock()
defer c.mu.Unlock()
if c.helperPCs == nil {
c.helperPCs = make(map[uintptr]struct{})
}
var pc [1]uintptr
n := runtime.Callers(2, pc[:])
if n == 0 {
panic("testing: zero callers found")
}
if _, found := c.helperPCs[pc[0]]; !found {
c.helperPCs[pc[0]] = struct{}{}
c.helperNames = nil
}
}
```
### Why
When a test helper calls `t.Helper()`, failures report the **caller's** line number, not the helper's. Without it, every failure points to the helper function — useless for identifying which test case failed.
### Idiomatic Usage
```go
func assertNoError(t *testing.T, err error) {
t.Helper() // failures point to the caller, not this line
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func assertEqual(t *testing.T, got, want any) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
```
### Anti-pattern
```go
// DON'T: Forget t.Helper() in test helpers
func checkResult(t *testing.T, got, want string) {
// Missing t.Helper()
if got != want {
t.Errorf("got %q, want %q", got, want)
// Error points HERE, not the actual test case — confusing
}
}
```
---
## 3. t.Run() — Subtests and Test Organization
### Source: `src/testing/testing.go:2204-2260`
```go
// src/testing/testing.go:2204-2215
func (t *T) Run(name string, f func(t *T)) bool {
t.hasSub.Store(true)
testName, ok, _ := t.tstate.match.fullName(&t.common, name)
if !ok || shouldFailFast() {
return true
}
// ...
ctx, cancelCtx := context.WithCancel(context.Background())
t = &T{
common: common{
name: testName,
parent: &t.common,
// ...
},
}
go tRunner(t, f)
// ...
}
```
### Why
Subtests:
1. **Run in separate goroutines** — isolated from each other
2. **Have their own context** — `context.WithCancel(context.Background())`
3. **Can be filtered** — `go test -run TestFoo/subcase`
4. **Can run in parallel** — via `t.Parallel()` inside the subtest
5. **Share setup/teardown** — parent test's defer runs after all subtests
### Pattern: Setup/Teardown with Subtests
```go
func TestDB(t *testing.T) {
db := setupTestDB(t) // shared setup
t.Cleanup(func() { db.Close() }) // runs after ALL subtests
t.Run("Insert", func(t *testing.T) {
// uses db
})
t.Run("Query", func(t *testing.T) {
// uses db
})
}
```
### Anti-pattern
```go
// DON'T: Rely on test execution order
func TestInsert(t *testing.T) { ... } // must run before TestQuery
func TestQuery(t *testing.T) { ... } // depends on TestInsert's side effects
// Tests should be independent!
```
---
## 4. t.Parallel() — Concurrent Test Execution
### Source: `src/testing/testing.go:1912`
```go
// src/testing/testing.go:1912
func (t *T) Parallel() {
// ...marks test as parallel, pauses until parent completes
}
```
### Why
`t.Parallel()` signals that this test can run concurrently with other parallel tests. The test pauses until its parent test function returns, then runs alongside other parallel subtests.
### Idiomatic Usage
```go
func TestFoo(t *testing.T) {
tests := []struct{
name string
input int
}{
{"small", 1},
{"large", 1000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // runs concurrently with other subtests
result := expensiveOperation(tt.input)
if result != expected {
t.Errorf(...)
}
})
}
}
```
### Anti-pattern
```go
// DON'T: Parallel tests that share mutable state
var counter int
func TestA(t *testing.T) {
t.Parallel()
counter++ // DATA RACE
}
func TestB(t *testing.T) {
t.Parallel()
counter++ // DATA RACE
}
// DON'T: Capture loop variable in Go < 1.22 without explicit copy
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// In Go < 1.22: tt is shared — always sees last value
// In Go >= 1.22: loop variables are per-iteration (fixed)
})
}
```
---
## 5. t.Cleanup() — Deterministic Teardown
### Source: `src/testing/testing.go:1439`
```go
// src/testing/testing.go:1439-1442
// Cleanup registers a function to be called when the test (or subtest)
// and all its subtests complete. Cleanup functions will be called in
// last added, first called order.
func (c *common) Cleanup(f func()) { ... }
```
### Why
`t.Cleanup` is like `defer` but tied to test lifecycle, not function scope. It runs after all subtests complete and works with parallel tests.
### Idiomatic Usage
```go
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
// Caller doesn't need to worry about cleanup:
func TestQueries(t *testing.T) {
db := setupTestDB(t) // automatically cleaned up
// ...
}
```
### Anti-pattern
```go
// DON'T: Return cleanup functions (easy to forget)
func setupDB(t *testing.T) (*sql.DB, func()) {
db := ...
return db, func() { db.Close() }
}
// Caller must remember: db, cleanup := setupDB(t); defer cleanup()
// DO: Use t.Cleanup inside the setup function
```
---
## 6. t.TempDir() — Automatic Temp Directories
### Source: `src/testing/testing.go:1575`
```go
// src/testing/testing.go:1575
func (c *common) TempDir() string { ... }
```
### Why
Creates a temp directory that's automatically removed when the test completes. No manual cleanup needed, no leftover test artifacts.
### Idiomatic Usage
```go
func TestWriteConfig(t *testing.T) {
dir := t.TempDir() // auto-cleaned
path := filepath.Join(dir, "config.json")
err := WriteConfig(path, myConfig)
if err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(path)
// assert contents...
}
```
### Anti-pattern
```go
// DON'T: Create temp dirs manually and forget cleanup
func TestWrite(t *testing.T) {
dir, _ := os.MkdirTemp("", "test")
// forgot os.RemoveAll(dir) — test leaves garbage
}
```
---
## 7. testdata/ Directory
### Source: `src/net/http/testdata/`, `src/encoding/json/testdata/` (implicit — exists in the tree)
```
src/net/http/testdata/
├── file
├── index.html
└── style.css
```
### Why
The `testdata/` directory is special in Go:
1. **Ignored by the go tool** — not compiled as a package
2. **Available to tests** — accessed via relative path `"testdata/file.txt"`
3. **Committed to repo** — test fixtures live alongside the code
4. **Portable** — tests work without external dependencies
### Idiomatic Usage
```go
func TestParse(t *testing.T) {
input, err := os.ReadFile("testdata/input.json")
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile("testdata/expected.json")
if err != nil {
t.Fatal(err)
}
got := Parse(input)
if !bytes.Equal(got, want) {
t.Errorf("Parse mismatch")
}
}
```
### Golden File Pattern
```go
func TestOutput(t *testing.T) {
got := generateOutput()
golden := filepath.Join("testdata", t.Name()+".golden")
if *update { // -update flag to regenerate
os.WriteFile(golden, got, 0644)
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Errorf("output mismatch; run with -update to regenerate")
}
}
```
### Anti-pattern
```go
// DON'T: Embed large test fixtures as string literals
var testInput = `{
"very": "long",
"json": "string",
// 500 lines...
}`
// DON'T: Depend on external URLs for test data
func TestParse(t *testing.T) {
resp, _ := http.Get("https://example.com/test.json") // flaky!
}
```
---
## 8. Error Message Formatting
### Source: `src/encoding/json/encode_test.go` (throughout)
```go
// Pattern from encode_test.go:304
t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err)
// Pattern from encode_test.go:306-307
t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, got, want)
// Pattern from encode_test.go:421-422
t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError))
```
### Why
The stdlib follows a consistent error format:
- **Context first** — where/what was being tested
- **got/want on separate lines** — easy to diff visually
- **Tab-indented** — aligns the comparison
### Convention
```
t.Errorf("FunctionName(%v) = %v, want %v", input, got, want)
// or for complex values:
t.Errorf("FunctionName(%v):\n\tgot: %v\n\twant: %v", input, got, want)
```
### Anti-pattern
```go
// DON'T: Vague error messages
t.Error("failed") // what failed? what was expected?
// DON'T: Only show the got value
t.Errorf("got %v", got) // what was expected?
// DON'T: Use assert libraries that hide the actual comparison
assert.Equal(t, got, want) // when it fails: "not equal" — which is which?
```
---
## 9. t.Fatal vs t.Error
### Convention across stdlib
```go
// Fatal: test cannot continue meaningfully
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err) // no point continuing without a database
}
// Error: test can continue, report all failures
for _, tt := range tests {
got := fn(tt.input)
if got != tt.want {
t.Errorf(...) // report and keep going
}
}
```
### Why
- `t.Fatal` / `t.Fatalf` — **stops the test immediately**. Use for setup failures where subsequent assertions are meaningless.
- `t.Error` / `t.Errorf` — **reports failure, continues**. Use in loops to collect all failures at once.
### Anti-pattern
```go
// DON'T: Fatal in loops (misses subsequent failures)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got != tt.want {
t.Fatalf(...) // stops this subtest — fine in subtests actually
}
})
}
// But without subtests:
for _, tt := range tests {
if got != tt.want {
t.Fatalf(...) // stops ALL remaining cases!
}
}
// DON'T: Error when you can't continue
file, err := os.Open(path)
if err != nil {
t.Errorf("open: %v", err) // continues to use nil file — panic!
}
file.Read(...)
```
---
## 10. Test Naming Conventions
### Source: All stdlib tests follow these patterns
```go
// Function tests: TestFunctionName
func TestMarshal(t *testing.T) { ... }
// Method tests: TestTypeName_MethodName
func TestEncoder_Encode(t *testing.T) { ... }
// Behavior tests: TestDescription
func TestOmitEmpty(t *testing.T) { ... }
// Edge cases: TestFunctionName_EdgeCase
func TestMarshal_NilSlice(t *testing.T) { ... }
// Benchmarks: BenchmarkFunctionName
func BenchmarkMarshal(b *testing.B) { ... }
// Examples: ExampleFunctionName
func ExampleMarshal() {
// ...
// Output: {"name":"Alice"}
}
```
### Why
- Test names are used with `-run` flag for filtering
- They appear in failure output — should be self-explanatory
- Example functions become documentation (shown in godoc)
- Subtests use `/` separator: `TestMarshal/NilSlice`
### Anti-pattern
```go
// DON'T: Numbered tests
func Test1(t *testing.T) { ... }
func Test2(t *testing.T) { ... }
// DON'T: Tests that don't start with Test
func testHelper(t *testing.T) { ... } // won't be run! (lowercase 't')
// DON'T: Overly verbose names
func TestThatMarshalCorrectlyHandlesNilSliceInputAndReturnsNullJSON(t *testing.T) { ... }
```
---
## Summary: Testing Best Practices
| Pattern | When |
|---------|------|
| Table-driven tests | Multiple inputs for same logic |
| t.Run subtests | Isolate cases, enable `-run` filtering |
| t.Helper() | Every test helper function |
| t.Parallel() | Independent tests, speed up suite |
| t.Cleanup() | Resource teardown (replaces defer in helpers) |
| t.TempDir() | Need filesystem for test |
| testdata/ | External test fixtures |
| Golden files | Complex expected output |
| t.Fatal | Setup failures (can't continue) |
| t.Error | Assertion failures (collect all) |
| got/want format | `"got %v, want %v"` or `"\n\tgot: %v\n\twant: %v"` |
-262
View File
@@ -1,262 +0,0 @@
# Anti-Patterns: What Kubernetes Avoids (and Why)
## 1. Never Mutate Shared Cache Objects
**What they avoid:** Modifying objects returned by Listers/Informers without deep-copying first.
**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
**What they avoid:** Multiple goroutines syncing the same object simultaneously.
**Why:** Two goroutines reading the same Deployment, each computing a different desired state, then both writing → conflict errors and potential state corruption.
**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:
```go
// From queue.go — the processing set blocks concurrent access
func (q *Typed[T]) Get() (item T, shutdown bool) {
// ...
q.processing.Insert(item) // Mark as being worked on
q.dirty.Delete(item)
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 {
dc.queue.AddRateLimited(key)
return
}
dc.queue.Forget(key) // Also forget when giving up
}
```
---
## 5. Never Hit the API Server in a Tight Loop
**What they avoid:** Direct API calls for reads. List/Get calls in hot paths.
**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.
**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
**What they avoid:** Processing items before the informer has done its initial List.
**Why:** With an empty cache, a controller might think "no pods exist, must create all of them" — causing a thundering herd of duplicate creates.
**The pattern K8s enforces:**
```go
// pkg/controller/deployment/deployment_controller.go:189
if !cache.WaitForNamedCacheSyncWithContext(ctx,
dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) {
return // Don't start workers until all caches are populated
}
```
---
## 7. Never Ignore Tombstones in Delete Handlers
**What they avoid:** Assuming delete handlers always receive the concrete type.
**Why:** If a watch disconnects and reconnects, missed deletes arrive as `DeletedFinalStateUnknown` (tombstones). Ignoring them means your controller never learns about those deletions.
**The pattern K8s enforces:**
```go
// Every delete handler must check for tombstones
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(...)
return
}
d, ok = tombstone.Obj.(*apps.Deployment)
// ...
}
}
```
---
## 8. Never Use ResourceVersion for Equality
**What they avoid:** Comparing ResourceVersion to check if an object changed.
**Why:** ResourceVersion is opaque (currently etcd's mod_revision, but this is an implementation detail). The only valid operation is "!=" to detect change.
**The pattern K8s uses:**
```go
// pkg/controller/deployment/deployment_controller.go:284-288
func (dc *DeploymentController) updateReplicaSet(logger klog.Logger, old, cur interface{}) {
curRS := cur.(*apps.ReplicaSet)
oldRS := old.(*apps.ReplicaSet)
if curRS.ResourceVersion == oldRS.ResourceVersion {
return // Periodic resync, nothing actually changed
}
// ... process the real update
}
```
---
## 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:
func BackoffUntilWithContext(ctx context.Context, f func(ctx context.Context), ...) {
func() {
defer runtime.HandleCrashWithContext(ctx) // Per-iteration recovery
f(ctx)
}()
}
```
---
## 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 m.canAdoptErr
}
```
---
## 12. Never Expose Mutable State Through Interfaces
**What they avoid:** Returning pointers to internal state through public interfaces.
**Why:** Callers can accidentally mutate internal state, creating subtle bugs that only manifest under concurrency.
**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.
---
## Summary: The Philosophy
Kubernetes avoids these anti-patterns because of one fundamental truth: **in a distributed system, every assumption you make about state being consistent is wrong.**
The patterns exist because:
1. **Events are unreliable** → level-triggered reconciliation
2. **Reads are stale** → always compare desired vs actual
3. **Concurrent access is inevitable** → deep copy, queue serialization
4. **Failures are normal** → retry with backoff, graceful degradation
5. **Resources are shared** → cache reads, rate-limit writes
6. **Systems outlive their authors** → code generation, type registries, feature gates
+558
View File
@@ -0,0 +1,558 @@
# Common Go Mistakes and Code Smells
Patterns that Go programmers get wrong, extracted from what the Go stdlib and Kubernetes intentionally **avoid** or handle carefully.
---
## 1. Interface Pollution
### Pattern/Smell Name: Premature/Over-broad Interfaces
**Source citation:** `/tmp/go-src/src/io/io.go` lines 86-307 (22 interfaces, each 1-2 methods), `/tmp/go-src/src/net/http/server.go` lines 89-95 (Handler: 1 method)
**What they do:** Every stdlib interface has 1-2 methods. `io.Reader` has one method. `http.Handler` has one method. Interfaces are defined at the **consumer** site (where they're used), not the producer site (where they're implemented).
**What they avoid:** Large interfaces, interfaces defined by the implementer, interfaces defined "just in case."
**Why:** Small interfaces are easy to implement and compose. Large interfaces force all implementations to satisfy every method — most of which the caller doesn't need. The Go proverb: "The bigger the interface, the weaker the abstraction."
**Anti-pattern (the smell):**
```go
// BAD: "Java-style" interface defined by the implementor
type UserService interface {
GetUser(id string) (*User, error)
CreateUser(u *User) error
UpdateUser(u *User) error
DeleteUser(id string) error
ListUsers(filter Filter) ([]*User, error)
GetUserByEmail(email string) (*User, error)
ValidateUser(u *User) error
// 15 more methods...
}
```
**Correct Go pattern:**
```go
// GOOD: Interface defined at the consumer with only what it needs
type UserGetter interface {
GetUser(id string) (*User, error)
}
// The handler only declares what it actually calls
func NewHandler(users UserGetter) *Handler { ... }
```
**Key rules from stdlib:**
- Accept interfaces, return structs
- Define interfaces where they're *consumed*, not where they're *produced*
- 1-2 methods per interface is ideal; 3 is suspicious; 5+ is almost certainly wrong
- Use `var _ Interface = (*Struct)(nil)` to verify implementations at compile time (server.go line 1802)
---
## 2. Error Swallowing
### Pattern/Smell Name: Ignoring Returned Errors
**Source citation:** `/tmp/go-src/src/net/http/server.go` (18 `if err != nil` blocks, every one handles the error), `/tmp/go-src/src/net/http/transport.go` (25 `if err != nil` blocks, all handled)
**What they do:** Every error returned from a function call is either:
1. Returned to the caller (propagated)
2. Logged and the operation retried/aborted
3. Wrapped with context via `fmt.Errorf("...: %w", err)`
The stdlib **never** does `_ = someFunc()` on functions that return errors (with rare, documented exceptions).
**Why:** Swallowed errors create silent failures. A database write that silently fails. A file close that leaks a descriptor. A network send that drops data. These manifest as data corruption or resource exhaustion hours later.
**Anti-pattern (the smell):**
```go
// BAD: Error thrown away
f.Close() // Close can fail (unflushed writes)
json.Unmarshal(data, &result) // silently leaves result zero-valued
resp, _ := http.Get(url) // panic on nil resp
// BAD: "log and forget" where the caller can't tell it failed
func SaveUser(u *User) {
if err := db.Save(u); err != nil {
log.Printf("failed to save user: %v", err)
// caller thinks it succeeded!
}
}
```
**Correct Go pattern:**
```go
// GOOD: Error propagated with context
func SaveUser(u *User) error {
if err := db.Save(u); err != nil {
return fmt.Errorf("save user %s: %w", u.ID, err)
}
return nil
}
// GOOD: Close errors handled for writes
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
```
---
## 3. Naked Goroutines Without Shutdown
### Pattern/Smell Name: Goroutine Leaks from Unmanaged Spawning
**Source citation:** `/tmp/go-src/src/net/http/transport.go` lines 2111-2112 (readLoop/writeLoop with closech), `/tmp/go-src/src/net/http/server.go` lines 3553 (serve with context cancellation), `/tmp/go-src/src/net/http/main_test.go` (goroutine leak detector)
**What they do:** Every goroutine spawned in the stdlib has a clear shutdown path:
- A channel that signals termination (`closech`, `done`)
- A context that gets cancelled
- A `sync.WaitGroup` that tracks completion
- Tests that verify no goroutines leak
The stdlib HTTP package spawns goroutines (readLoop, writeLoop, serve) but each one:
1. Has a `closech` channel or context for signaling
2. Has a `writeLoopDone` channel to confirm exit
3. Is tracked by `sync.WaitGroup` (in httptest.Server)
4. Is verified by `afterTest` goroutine leak detection
**Why:** A goroutine without a shutdown path runs forever. In a server handling 10K connections, that's 10K leaked goroutines per restart cycle. Go's garbage collector cannot collect goroutines — they must exit.
**Anti-pattern (the smell):**
```go
// BAD: Goroutine with no way to stop it
func StartWorker(ch <-chan Task) {
go func() {
for task := range ch {
process(task)
}
}()
// Who closes ch? What if nobody does? Goroutine leaks forever.
}
// BAD: Fire-and-forget goroutine
func HandleRequest(r *Request) {
go sendAnalytics(r) // What if this blocks? No timeout, no tracking.
}
```
**Correct Go pattern (from stdlib transport.go):**
```go
// GOOD: Goroutine with explicit lifecycle
type persistConn struct {
reqch chan requestAndChan // communication
closech chan struct{} // signal shutdown
writeLoopDone chan struct{} // confirm exit
}
go pconn.readLoop() // reads from closech to know when to stop
go pconn.writeLoop() // closes writeLoopDone on exit
// Shutdown:
func (pc *persistConn) close(err error) {
close(pc.closech) // signal both loops
<-pc.writeLoopDone // wait for confirmation
}
```
---
## 4. sync.Mutex Where atomic Suffices
### Pattern/Smell Name: Over-synchronization with Mutexes
**Source citation:** `/tmp/go-src/src/net/http/server.go` lines 298-300 (atomic for simple state), `/tmp/go-src/src/net/http/transport.go` line 786-787 (comment explaining why NOT atomic)
**What they do:** Use `atomic.Bool`, `atomic.Pointer`, `atomic.Uint64` for simple flags and state that is read/written independently. Reserve `sync.Mutex` for guarding multi-field invariants.
The stdlib explicitly documents the decision: `didRead bool // not atomic.Bool because only one goroutine (the user's) should be accessing` (transport.go line 786).
**Why:** Mutexes serialize all access. For a single boolean flag read by many goroutines (like `inShutdown`), atomic operations are lock-free and orders of magnitude faster under contention.
**Anti-pattern (the smell):**
```go
// BAD: Mutex for a single boolean
type Server struct {
mu sync.Mutex
shutdown bool
}
func (s *Server) IsShutdown() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.shutdown
}
```
**Correct Go pattern (from stdlib):**
```go
// GOOD: Atomic for independent flag (server.go line 3144)
type Server struct {
inShutdown atomic.Bool
}
func (s *Server) shuttingDown() bool {
return s.inShutdown.Load()
}
// GOOD: Packed state in atomic (server.go line 300)
curState atomic.Uint64 // packed (unixtime<<8|uint8(ConnState))
// GOOD: Mutex only when multiple fields must be consistent together
mu sync.Mutex // guards hijackedv
hijackedv bool
```
**Rule of thumb:**
- Single value read/written independently → `atomic`
- Multiple values that must be consistent together → `sync.Mutex`
- Read-heavy, rare writes → `sync.RWMutex`
---
## 5. Channel Misuse
### Pattern/Smell Name: Channels for Simple Synchronization
**Source citation:** `/tmp/go-src/src/net/http/server.go` (uses channels for signaling/coordination, never for simple shared state), `/tmp/go-src/src/net/http/transport.go` lines 2242-2259 (channels with clear ownership documentation)
**What they do:**
- Channels for **signaling** events (closech, done)
- Channels for **passing ownership** of data between goroutines (reqch, writech)
- Comments documenting which goroutine reads/writes each channel
- Buffered channels with explicit reasoning about buffer size
**What they avoid:**
- Channels for sharing mutable state
- Unbuffered channels where buffered would prevent deadlock
- Channels where a mutex would be simpler
**Why:** Rob Pike's "Don't communicate by sharing memory; share memory by communicating" is often misunderstood as "always use channels." The stdlib uses mutexes freely when appropriate. Channels are for goroutine coordination, not data sharing.
**Anti-pattern (the smell):**
```go
// BAD: Channel as a glorified mutex
type Counter struct {
ch chan int
}
func NewCounter() *Counter {
c := &Counter{ch: make(chan int, 1)}
c.ch <- 0
return c
}
func (c *Counter) Increment() {
val := <-c.ch
c.ch <- val + 1
}
// BAD: Unbuffered channel causing goroutine leak
func doWork() <-chan Result {
ch := make(chan Result) // unbuffered!
go func() {
result := expensiveWork()
ch <- result // blocks forever if nobody reads
}()
return ch
}
```
**Correct Go pattern (from stdlib transport.go):**
```go
// GOOD: Channels with documented ownership and correct buffering
type persistConn struct {
reqch chan requestAndChan // written by roundTrip; read by readLoop
writech chan writeRequest // written by roundTrip; read by writeLoop
closech chan struct{} // closed when conn closed
writeErrCh chan error // buffer 1: passes write error from writeLoop to readLoop
}
// GOOD: Error channel buffered to 1 — writer never blocks
writeErrCh: make(chan error, 1)
```
---
## 6. init() Abuse
### Pattern/Smell Name: Overusing init() for Side Effects
**Source citation:** `/tmp/go-src/src/net/http/http2.go` lines 37-47 (one of the few init() uses — for package-level wiring that cannot be done any other way)
**What they do:** The stdlib uses `init()` sparingly and only for:
1. Registering protocol handlers with other packages (http2.go)
2. Setting up test hooks (export_test.go — only compiled during tests)
3. Package-level wiring that has no other option
The entire `net/http` package has only 3 `init()` functions in production code, and each has a comment explaining why it's necessary.
**Why:**
- `init()` runs implicitly — no one calls it, you can't control when, you can't skip it
- Makes testing harder (can't test without side effects)
- Creates import order dependencies
- Makes programs slow to start (all inits run at startup)
- Impossible to pass configuration to
**Anti-pattern (the smell):**
```go
// BAD: Database connection in init
func init() {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err) // crashes the program at import time!
}
globalDB = db
}
// BAD: Configuration in init
func init() {
cfg = loadConfig("/etc/myapp/config.yaml") // hard-coded path, untestable
}
// BAD: Registering handlers in init
func init() {
http.HandleFunc("/api/users", handleUsers) // global state mutation
}
```
**Correct Go pattern:**
```go
// GOOD: Explicit initialization with error handling
func NewApp(cfg Config) (*App, error) {
db, err := sql.Open("postgres", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
return &App{db: db}, nil
}
// GOOD: Only use init() for unavoidable package wiring (like stdlib does)
func init() {
// Must set these at init time because the types are in different packages
// and there's no other way to wire them without import cycles.
http2.NoBody = NoBody
}
```
---
## 7. Premature Concurrency
### Pattern/Smell Name: Goroutines Where Sequential Code Suffices
**Source citation:** `/tmp/go-src/src/encoding/json/` (zero goroutines in production code — purely sequential), `/tmp/go-src/src/net/http/server.go` (goroutines only at the connection accept level, not in handlers)
**What they do:** The stdlib only introduces concurrency where it's structurally necessary (handling multiple connections). JSON encoding, HTTP header parsing, cookie handling — all purely sequential despite being hot paths.
**Why:** Goroutines have overhead (stack allocation, scheduler pressure, synchronization). Sequential code is easier to reason about, debug, and profile. Concurrency should solve a structural problem (waiting for I/O, handling multiple clients) — not speed up CPU-bound work (that's `GOMAXPROCS`'s job).
**Anti-pattern (the smell):**
```go
// BAD: Goroutines for CPU-bound work that shares nothing
func ProcessItems(items []Item) []Result {
results := make([]Result, len(items))
var wg sync.WaitGroup
for i, item := range items {
wg.Add(1)
go func(i int, item Item) {
defer wg.Done()
results[i] = transform(item) // transform is a pure function!
}(i, item)
}
wg.Wait()
return results
}
// This spawns 10000 goroutines for 10000 items. A simple loop is faster.
```
**Correct Go pattern:**
```go
// GOOD: Sequential unless concurrency is structurally needed
func ProcessItems(items []Item) []Result {
results := make([]Result, len(items))
for i, item := range items {
results[i] = transform(item)
}
return results
}
// GOOD: Concurrency only when I/O-bound (as in the HTTP server)
for {
rw, err := l.Accept() // blocks waiting for connection (I/O)
go c.serve(connCtx) // each connection needs its own goroutine because it blocks on I/O
}
```
---
## 8. Interface Satisfaction Checks (What They DO)
### Pattern/Smell Name: Compile-Time Interface Verification
**Source citation:** `/tmp/go-src/src/net/http/server.go` line 1802, `/tmp/go-src/src/net/http/transport.go` line 2141
**What they do:** Use `var _ Interface = (*Struct)(nil)` to verify at compile time that a type implements an interface.
**Why:** Without this, you discover a missing method at runtime (when someone calls the interface method). The blank identifier assignment costs nothing at runtime but catches missing methods at compile time.
**Code example (stdlib):**
```go
// server.go line 1802
var _ closeWriter = (*net.TCPConn)(nil)
// transport.go line 2141
var _ io.ReaderFrom = (*persistConnWriter)(nil)
// server.go line 4071
var _ Pusher = (*timeoutWriter)(nil)
```
**Anti-pattern:** Relying on runtime panics to discover interface mismatches.
---
## 9. Error Wrapping Without Context
### Pattern/Smell Name: Bare Error Propagation
**Source citation:** `/tmp/go-src/src/net/http/transport.go` line 2395, `/tmp/go-src/src/net/http/request.go` line 96
**What they do:** Wrap errors with `fmt.Errorf("context: %w", err)` to add information about what operation failed. Each layer adds its context.
**Why:** A bare `return err` from deep in a call stack gives you "connection refused" with no indication of what was being connected to, for what purpose, with what parameters. Wrapped errors create a trace: `"save user alice: write to database: connection refused"`.
**Anti-pattern (the smell):**
```go
// BAD: Bare propagation
func GetUser(id string) (*User, error) {
data, err := db.Query(...)
if err != nil {
return nil, err // caller sees "connection refused" — useless
}
}
// BAD: Losing the original error
func GetUser(id string) (*User, error) {
data, err := db.Query(...)
if err != nil {
return nil, fmt.Errorf("database error") // original error is gone
}
}
```
**Correct Go pattern (from stdlib):**
```go
// GOOD: Wrapping with %w preserves the chain
// transport.go line 2395
return fmt.Errorf("net/http: HTTP/1.x transport connection broken: %w", err)
// GOOD: Custom error type with full context
// request.go line 96
func badStringError(what, val string) error {
return fmt.Errorf("%s %q", what, val)
}
```
---
## 10. Kubernetes-Scale Anti-Patterns
### Pattern/Smell Name: Patterns That Work at Scale but Smell at Small Scale
**Source citation:** `/tmp/kubernetes-src/pkg/util/iptables/iptables_test.go` (fakeexec), `/tmp/kubernetes-src/staging/src/k8s.io/client-go/` (testify usage)
**What Kubernetes does that you probably shouldn't:**
1. **Assertion libraries (testify)** — Kubernetes uses `assert.Equal`, `require.NoError` extensively. The Go team explicitly avoids these. At Kubernetes scale (4M+ lines), the consistency might help. At normal scale, plain `if/t.Errorf` gives better error messages.
2. **Fake executors** — Kubernetes fakes the entire `exec.Command` interface for testing iptables rules. This is necessary because they can't run iptables in CI. For most codebases, integration tests with real dependencies are more valuable than elaborate fakes.
3. **Generated deepcopy methods** — Kubernetes generates `DeepCopy()` on every API type. At 500+ types, this is necessary. At 10 types, just write the copy manually.
4. **Interface-everything for testing** — Kubernetes wraps system calls, time, filesystem behind interfaces purely for testability. At their scale, you can't spin up real infrastructure. At small scale, use `httptest.Server` and real databases in Docker.
**The lesson:** Patterns born from scale requirements become anti-patterns when applied at the wrong scale. Every abstraction has a cost; only pay it when you have the problem it solves.
---
## 11. Context Misuse
### Pattern/Smell Name: Storing Values in context.Context
**Source citation:** `/tmp/go-src/src/net/http/server.go` lines 1060-1080 (context used for cancellation, NOT for passing data between layers)
**What they do:** Use context for:
- Cancellation signals (`WithCancel`, `WithTimeout`)
- Deadline propagation
- Request-scoped values that cross API boundaries (only via well-typed keys)
**What they avoid:** Using context as a grab-bag for function parameters, replacing explicit arguments with context values.
**Anti-pattern (the smell):**
```go
// BAD: Context as parameter smuggling
ctx = context.WithValue(ctx, "userID", userID)
ctx = context.WithValue(ctx, "db", database)
ctx = context.WithValue(ctx, "logger", logger)
func HandleRequest(ctx context.Context) {
userID := ctx.Value("userID").(string) // type assertion panic risk
db := ctx.Value("db").(*sql.DB) // invisible dependency
}
```
**Correct Go pattern (from stdlib):**
```go
// GOOD: Context for cancellation, explicit params for data
func HandleRequest(ctx context.Context, userID string, db *sql.DB) {
// ...
}
// GOOD: If you must use context values, use typed keys
type contextKey struct{}
var serverContextKey = contextKey{}
// Only for cross-cutting concerns that truly cross API boundaries
// (tracing, auth tokens, request IDs)
```
---
## 12. Using fmt.Sprintf in Hot Paths
### Pattern/Smell Name: Allocation in Performance-Critical Code
**Source citation:** `/tmp/go-src/src/net/http/header.go` (hand-rolled header writing, no fmt), `/tmp/go-src/src/encoding/json/encode.go` (manual byte buffer manipulation)
**What they do:** The stdlib avoids `fmt.Sprintf`, `string concatenation (+)`, and `[]byte(string)` conversions in hot paths. Instead, they write directly to `[]byte` buffers or use `strings.Builder`.
**Why:** Each `fmt.Sprintf` allocates. In a server handling 100K requests/second, that's 100K allocations/second for a single log line. The GC notices.
**Anti-pattern (the smell):**
```go
// BAD: Allocation per request
func (h Header) Get(key string) string {
return fmt.Sprintf("%s", h[key][0]) // unnecessary allocation
}
// BAD: String concatenation in a loop
var result string
for _, item := range items {
result += item.Name + "," // O(n²) allocations
}
```
**Correct Go pattern:**
```go
// GOOD: Direct buffer writes (like stdlib does)
var buf strings.Builder
for _, item := range items {
buf.WriteString(item.Name)
buf.WriteByte(',')
}
return buf.String() // single allocation for final string
```