docs: add 'when to use' triggers + examples to all patterns

Added 'When to Use' subsections with concrete decision triggers and
before/after Go code examples to patterns across all directories:

- patterns/error-handling.md (3 patterns: sentinels, wrapping, Join)
- patterns/concurrency.md (4 patterns: Mutex, Once, done channels, pipelines)
- patterns/interfaces.md (4 patterns: small interfaces, accept/return, adapter, optional)
- patterns/structs.md (3 patterns: zero-value, constructors, config structs)
- patterns/package-design.md (3 patterns: internal/, init(), context keys)
- patterns/style.md (3 patterns: interface checks, iota constants, named types)
- patterns/testing-advanced.md (3 patterns: table tests, golden files, httptest)
- patterns/api-conventions.md (3 patterns: Must, layered API, graceful shutdown)
- patterns/documentation.md (2 patterns: examples, deprecated)
- kubernetes/patterns.md (3 patterns: controller, workqueue, leader election)
- kubernetes/production-go.md (2 patterns: codegen, HandleCrash)
- smells/anti-patterns.md (2 anti-patterns: cache mutation, edge-triggered)
This commit is contained in:
2026-04-30 12:07:40 +00:00
parent 0e5974f39a
commit eb9171368b
12 changed files with 1163 additions and 0 deletions
+117
View File
@@ -71,6 +71,41 @@ func (dc *DeploymentController) handleErr(ctx context.Context, err error, key st
}
```
### When to Use
**Triggers:**
- You're building a system that must maintain desired state over time (not just react to events once)
- External state can change outside your control (user edits, crashes, network partitions)
- You need automatic recovery from partial failures without human intervention
**Example — before:**
```go
// Event-driven: reacts once and hopes nothing changes
func handlePodCreated(pod Pod) {
assignToNode(pod)
// What if the node dies 5 seconds later? Nobody re-assigns.
}
```
**Example — after:**
```go
// Controller pattern: continuously reconciles desired vs actual
func (c *Scheduler) Reconcile(ctx context.Context, key string) error {
pod, err := c.podLister.Get(key)
if err != nil { return err }
if pod.Spec.NodeName == "" {
node := c.selectBestNode(pod)
return c.assignPodToNode(ctx, pod, node)
}
// Already assigned — verify node is still healthy
if !c.nodeIsReady(pod.Spec.NodeName) {
return c.reassignPod(ctx, pod)
}
return nil // desired state matches actual state
}
```
### 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
@@ -162,6 +197,51 @@ A concurrent-safe work queue with three critical properties:
### 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.
### When to Use
**Triggers:**
- Multiple event sources (informer callbacks) trigger work on the same object rapidly
- You need to deduplicate: 5 events for the same pod should result in 1 sync, not 5
- Failed processing should retry with exponential backoff, not flood the system
**Example — before:**
```go
// Raw channel: no deduplication, no backoff
events := make(chan string, 100)
// Producer fires rapid updates:
events <- "pod-abc" // event 1
events <- "pod-abc" // event 2 (duplicate!)
events <- "pod-abc" // event 3 (duplicate!)
// Consumer processes all 3 — wasteful
for key := range events {
reconcile(key) // called 3 times for the same stale state
}
```
**Example — after:**
```go
queue := workqueue.NewTypedRateLimitingQueue[string](
workqueue.DefaultTypedControllerRateLimiter[string](),
)
// Producer fires rapid updates — queue deduplicates:
queue.Add("pod-abc") // queued
queue.Add("pod-abc") // already dirty — no-op
queue.Add("pod-abc") // already dirty — no-op
// Consumer processes once with latest state:
key, _ := queue.Get()
err := reconcile(key) // called once
if err != nil {
queue.AddRateLimited(key) // retry with backoff
} else {
queue.Forget(key) // clear backoff counter
}
queue.Done(key)
```
### The Dirty/Processing Dance
```go
@@ -321,6 +401,43 @@ func (m *BaseControllerRefManager) ClaimObject(ctx context.Context, obj metav1.O
### 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.
### When to Use
**Triggers:**
- You're running multiple replicas of a controller for high availability
- Only ONE instance should actively reconcile at a time (to avoid conflicts)
- You need automatic failover: if the leader dies, another replica takes over within seconds
**Example — before:**
```go
// All replicas reconcile simultaneously → write conflicts, duplicate work
func main() {
ctrl := NewController()
ctrl.Run(ctx) // every replica does this — chaos
}
```
**Example — after:**
```go
func main() {
ctrl := NewController()
leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
Lock: resourceLock,
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
Callbacks: leaderelection.LeaderCallbacks{
OnStartedLeading: func(ctx context.Context) {
ctrl.Run(ctx) // only the leader reconciles
},
OnStoppedLeading: func() {
log.Fatal("lost leadership") // restart to re-enter election
},
},
})
}
```
### Why
Controller-manager runs multiple replicas for HA. Only one should reconcile to avoid conflicts.
+72
View File
@@ -52,6 +52,37 @@ Generated informers (note the header comment):
// Code generated by informer-gen. DO NOT EDIT.
```
### When to Use
**Triggers:**
- You have 10+ types that need identical boilerplate methods (DeepCopy, Validate, Marshal)
- Hand-writing the code is error-prone (forgetting to copy a new field causes silent bugs)
- The generated output is mechanical and reviewable, not creative
**Example — before:**
```go
// Hand-written deep copy for every type — 50 types × 30 lines each = 1500 lines of bugs
func (in *Deployment) DeepCopy() *Deployment {
out := new(Deployment)
out.Name = in.Name
out.Labels = make(map[string]string)
for k, v := range in.Labels { out.Labels[k] = v }
// Did you remember Annotations? Finalizers? Every nested struct?
}
```
**Example — after:**
```go
// +k8s:deepcopy-gen=true
type Deployment struct {
Name string
Labels map[string]string
Annotations map[string]string
}
// Generated: zz_generated.deepcopy.go handles ALL fields correctly, always.
// Adding a new field? Re-run generator. Zero chance of forgetting.
```
### 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.
@@ -329,6 +360,47 @@ In a production system with hundreds of goroutines, an unrecovered panic in one
- Allows cleanup handlers (shutdown gracefully)
- In tests, can be configured to not actually crash
### When to Use
**Triggers:**
- You're running multiple independent subsystems in one process (multiple controllers, background workers)
- A panic in one subsystem shouldn't kill the entire process
- You need structured logging of panic stack traces before potential recovery
**Example — before:**
```go
// One bad nil pointer in workerB kills workerA, workerC, and the whole server
func main() {
go workerA(ctx)
go workerB(ctx) // panics → entire process dies
go workerC(ctx)
select {}
}
```
**Example — after:**
```go
func safeGo(ctx context.Context, name string, f func(ctx context.Context)) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in %s: %v
%s", name, r, debug.Stack())
// Log, alert, increment metric — but don't kill siblings
}
}()
f(ctx)
}()
}
func main() {
safeGo(ctx, "worker-a", workerA)
safeGo(ctx, "worker-b", workerB) // panics → logged, other workers continue
safeGo(ctx, "worker-c", workerC)
select {}
}
```
### 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.