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:
@@ -38,6 +38,39 @@ func (m *Mutex) Lock() {
|
||||
- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock
|
||||
- **Locker interface** — abstracts over Mutex and RWMutex
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- Multiple goroutines read AND write the same data structure
|
||||
- You need to protect a small critical section (a few field accesses)
|
||||
- A channel-based solution would add complexity without benefit (no coordination needed, just protection)
|
||||
|
||||
**Example — before:**
|
||||
```go
|
||||
type Stats struct {
|
||||
hits int
|
||||
misses int
|
||||
}
|
||||
|
||||
func (s *Stats) RecordHit() { s.hits++ } // DATA RACE when called from multiple goroutines
|
||||
func (s *Stats) RecordMiss() { s.misses++ } // DATA RACE
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```go
|
||||
type Stats struct {
|
||||
mu sync.Mutex
|
||||
hits int
|
||||
misses int
|
||||
}
|
||||
|
||||
func (s *Stats) RecordHit() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.hits++
|
||||
}
|
||||
```
|
||||
|
||||
### Idiomatic Usage
|
||||
|
||||
```go
|
||||
@@ -101,6 +134,40 @@ The implementation reveals a subtle guarantee: **when Do returns, f has finished
|
||||
|
||||
The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27).
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You have expensive initialization that should happen exactly once (DB connection, config parse, compiled regex)
|
||||
- Multiple goroutines may trigger the initialization concurrently
|
||||
- You're using `var` + `if instance == nil` checks that aren't goroutine-safe
|
||||
|
||||
**Example — before:**
|
||||
```go
|
||||
var db *sql.DB
|
||||
|
||||
func GetDB() *sql.DB {
|
||||
if db == nil { // RACE: two goroutines can both see nil
|
||||
db, _ = sql.Open("postgres", connStr)
|
||||
}
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```go
|
||||
var (
|
||||
db *sql.DB
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
func GetDB() *sql.DB {
|
||||
once.Do(func() {
|
||||
db, _ = sql.Open("postgres", connStr)
|
||||
})
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
### Idiomatic Usage
|
||||
|
||||
```go
|
||||
@@ -297,6 +364,51 @@ case result := <-work:
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You need to broadcast "stop" to multiple goroutines simultaneously
|
||||
- A goroutine needs to select between work and cancellation
|
||||
- You're implementing graceful shutdown for a long-running service
|
||||
|
||||
**Example — before:**
|
||||
```go
|
||||
type Server struct {
|
||||
stopped bool // RACE: no synchronization
|
||||
}
|
||||
|
||||
func (s *Server) worker() {
|
||||
for {
|
||||
if s.stopped { return } // busy-polls, racy
|
||||
doWork()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```go
|
||||
type Server struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
return &Server{done: make(chan struct{})}
|
||||
}
|
||||
|
||||
func (s *Server) worker() {
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case work := <-s.workCh:
|
||||
process(work)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Stop() { close(s.done) } // broadcasts to ALL workers
|
||||
```
|
||||
|
||||
### Anti-pattern
|
||||
|
||||
```go
|
||||
@@ -494,6 +606,48 @@ func generate(ctx context.Context) <-chan int {
|
||||
}
|
||||
```
|
||||
|
||||
### When to Use
|
||||
|
||||
**Triggers:**
|
||||
- You have a producer-consumer flow where the consumer's speed should limit the producer (backpressure)
|
||||
- Data flows through multiple transformation stages
|
||||
- You want to decouple stages that can run concurrently
|
||||
|
||||
**Example — before:**
|
||||
```go
|
||||
func processAll(items []string) []Result {
|
||||
var results []Result
|
||||
for _, item := range items {
|
||||
fetched := fetch(item) // sequential: fetch then transform
|
||||
results = append(results, transform(fetched))
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
**Example — after:**
|
||||
```go
|
||||
func processAll(ctx context.Context, items []string) []Result {
|
||||
fetched := make(chan Fetched)
|
||||
go func() {
|
||||
defer close(fetched)
|
||||
for _, item := range items {
|
||||
select {
|
||||
case fetched <- fetch(item): // backpressure: blocks if transform is slow
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var results []Result
|
||||
for f := range fetched {
|
||||
results = append(results, transform(f))
|
||||
}
|
||||
return results
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-pattern
|
||||
|
||||
```go
|
||||
|
||||
Reference in New Issue
Block a user