diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e1ef96 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# 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. diff --git a/changelog/.gitkeep b/changelog/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/patterns/concurrency.md b/patterns/concurrency.md new file mode 100644 index 0000000..c57733f --- /dev/null +++ b/patterns/concurrency.md @@ -0,0 +1,595 @@ +# Go Concurrency Patterns + +Patterns extracted from the Go standard library source code. + +--- + +## 1. sync.Mutex — The Basic Lock + +### Source: `src/sync/mutex.go:18-34`, `src/sync/mutex.go:42-67` + +```go +// src/sync/mutex.go:18-34 +// A Mutex is a mutual exclusion lock. +// The zero value for a Mutex is an unlocked mutex. +// +// A Mutex must not be copied after first use. +type Mutex struct { + _ noCopy + mu isync.Mutex +} + +// src/sync/mutex.go:36-39 +type Locker interface { + Lock() + Unlock() +} + +// src/sync/mutex.go:43-46 +func (m *Mutex) Lock() { + m.mu.Lock() +} +``` + +### Why + +- **Zero value is ready to use** — no constructor needed +- **Must not be copied** — enforced by `noCopy` field (go vet detects copies) +- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock +- **Locker interface** — abstracts over Mutex and RWMutex + +### Idiomatic Usage + +```go +var mu sync.Mutex +mu.Lock() +defer mu.Unlock() +// ... critical section ... +``` + +### Anti-pattern + +```go +// DON'T: Copy a mutex +type Config struct { + mu sync.Mutex + data map[string]string +} +c2 := *c1 // COPIES the mutex — data race + +// DON'T: Forget defer +mu.Lock() +doSomething() // if this panics, mutex stays locked forever +mu.Unlock() +``` + +--- + +## 2. sync.Once — Exactly-Once Initialization + +### Source: `src/sync/once.go:12-36`, `src/sync/once.go:56-79` + +```go +// src/sync/once.go:12-23 +type Once struct { + _ noCopy + done atomic.Bool + m Mutex +} + +// src/sync/once.go:56-63 +func (o *Once) Do(f func()) { + if !o.done.Load() { + o.doSlow(f) + } +} + +// src/sync/once.go:65-72 +func (o *Once) doSlow(f func()) { + o.m.Lock() + defer o.m.Unlock() + if !o.done.Load() { + defer o.done.Store(true) + f() + } +} +``` + +### 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 `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27). + +### Idiomatic Usage + +```go +var ( + instance *DB + once sync.Once +) + +func GetDB() *DB { + once.Do(func() { + instance = connectToDB() + }) + return instance +} +``` + +### Anti-pattern + +```go +// DON'T: Call Do recursively (deadlocks) +var once sync.Once +once.Do(func() { + once.Do(func() { /* deadlock */ }) +}) +``` + +--- + +## 3. sync.WaitGroup — Waiting for Goroutine Completion + +### Source: `src/sync/waitgroup.go:14-43`, `src/sync/waitgroup.go:236-260` + +```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: +// +// var wg sync.WaitGroup +// wg.Go(task1) +// wg.Go(task2) +// wg.Wait() +type WaitGroup struct { + noCopy noCopy + state atomic.Uint64 + sema uint32 +} +``` + +### Go 1.25+: WaitGroup.Go + +```go +// src/sync/waitgroup.go:236-260 +func (wg *WaitGroup) Go(f func()) { + wg.Add(1) + go func() { + defer func() { + if x := recover(); x != nil { + // Don't call Done — let panic propagate fatally. + panic(x) + } + wg.Done() + }() + f() + }() +} +``` + +### 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. + +### Classic Pattern (pre-Go 1.25) + +```go +var wg sync.WaitGroup +for _, item := range items { + wg.Add(1) + go func() { + defer wg.Done() + process(item) + }() +} +wg.Wait() +``` + +### Anti-pattern + +```go +// DON'T: Add inside the goroutine (race with Wait) +for _, item := range items { + go func() { + wg.Add(1) // might run after Wait is called! + defer wg.Done() + process(item) + }() +} +wg.Wait() +``` + +--- + +## 4. sync.Pool — Object Reuse for GC Pressure + +### Source: `src/sync/pool.go:44-63` + +```go +// src/sync/pool.go:44-63 +// 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. +type Pool struct { + noCopy noCopy + local unsafe.Pointer + localSize uintptr + victim unsafe.Pointer + victimSize uintptr + New func() any +} +``` + +### Why + +Pool is **not** a general cache. Items can vanish between GC cycles. Use for reducing allocation pressure on hot paths. + +### Idiomatic Usage (from fmt package) + +```go +var ppFree = sync.Pool{ + New: func() any { return new(pp) }, +} + +func newPrinter() *pp { + p := ppFree.Get().(*pp) + p.reset() + return p +} + +func (p *pp) free() { + p.buf = p.buf[:0] // reset before returning + ppFree.Put(p) +} +``` + +### Anti-pattern + +```go +// DON'T: Use Pool for connection pooling (items disappear!) +var connPool = sync.Pool{ + New: func() any { return connectToDB() }, +} +// Connections may be GC'd — use database/sql's pool instead + +// DON'T: Put dirty objects back without resetting +pool.Put(buf) // still has data from last use +``` + +--- + +## 5. Channel as Done Signal (Context Pattern) + +### Source: `src/context/context.go:83-100` (Done channel), `src/io/pipe.go:42-45` + +```go +// src/context/context.go:83-100 +// Done returns a channel that's closed when work done on behalf of this +// context should be canceled. +Done() <-chan struct{} + +// src/io/pipe.go:42-45 +type pipe struct { + once sync.Once + done chan struct{} // closed on pipe close +} +``` + +### Why + +A `chan struct{}` costs zero bytes per send and closing it broadcasts to all receivers simultaneously. This is the canonical "done" signal in Go. + +```go +select { +case <-ctx.Done(): + return ctx.Err() +case result := <-work: + return result, nil +} +``` + +### Anti-pattern + +```go +// DON'T: Use chan bool for done signals +done := make(chan bool) // wastes 1 byte, true/false meaningless + +// DON'T: Send to done (only unblocks one receiver) +done <- struct{}{} + +// DO: Close the channel (broadcasts to all) +close(done) +``` + +--- + +## 6. Context Propagation Rules + +### Source: `src/context/context.go:37-48` + +```go +// src/context/context.go:37-48 +// 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: +// +// func DoSomething(ctx context.Context, arg Arg) error { +// // ... use ctx ... +// } +// +// Do not pass a nil Context, even if a function permits it. Pass context.TODO +// if you are unsure about which Context to use. +``` + +### Why + +Context flows **down** the call chain, never stored in structs. It carries deadlines and cancellation signals for the current request, not persistent state. + +### Anti-pattern + +```go +// DON'T: Store context in a struct +type Server struct { + ctx context.Context // stale context persists beyond request +} + +// DON'T: Pass nil +doWork(nil, data) // use context.TODO() if unsure + +// DON'T: Put context anywhere other than first parameter +func doWork(data Data, ctx context.Context) // wrong position +``` + +--- + +## 7. Context Cancellation with Timeout + +### Source: `src/net/http/server.go:4007-4050` (TimeoutHandler) + +```go +// src/net/http/server.go:4011-4050 +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 + case <-ctx.Done(): + // timeout — write 503 + } +} +``` + +### 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 + +### Anti-pattern + +```go +// DON'T: Forget to call cancel (leaks timer goroutines) +ctx, _ := context.WithTimeout(parent, 5*time.Second) + +// DON'T: Ignore context in long operations +func longWork(ctx context.Context) { + time.Sleep(10 * time.Minute) // ignores cancellation +} +``` + +--- + +## 8. Select with Non-Blocking Check + +### Source: `src/io/pipe.go:51-60` + +```go +// src/io/pipe.go:51-60 +func (p *pipe) read(b []byte) (n int, err error) { + select { + case <-p.done: + return 0, p.readCloseError() + default: + } + + select { + case bw := <-p.wrCh: + nr := copy(b, bw) + p.rdCh <- nr + return nr, nil + case <-p.done: + return 0, p.readCloseError() + } +} +``` + +### 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. + +### Anti-pattern + +```go +// DON'T: Check ctx.Done() in a busy loop +for { + select { + case <-ctx.Done(): + return + default: + } + // busy-spins CPU at 100%! +} +``` + +--- + +## 9. Channel Pipeline (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 { + wrCh chan []byte // writer sends data slices + rdCh chan int // reader returns bytes consumed + done chan struct{} +} + +// 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 + done: make(chan struct{}), + }}} + return &pw.r, pw +} +``` + +### Why + +`io.Pipe` uses **unbuffered channels** — each Write blocks until Read consumes. Backpressure is automatic. The `done` channel signals shutdown. + +### Pipeline Pattern Template + +```go +func generate(ctx context.Context) <-chan int { + out := make(chan int) + go func() { + defer close(out) + for i := 0; ; i++ { + select { + case out <- i: + case <-ctx.Done(): + return + } + } + }() + return out +} +``` + +### Anti-pattern + +```go +// DON'T: Forget to close channels (receivers block forever) +func produce() <-chan int { + ch := make(chan int) + go func() { + for i := 0; i < 10; i++ { + ch <- i + } + // forgot close(ch) — range receivers hang + }() + return ch +} +``` + +--- + +## 10. Background Worker with Context Shutdown + +### Source: `src/database/sql/sql.go:836-843` + +```go +// src/database/sql/sql.go:836-843 +func OpenDB(c driver.Connector) *DB { + ctx, cancel := context.WithCancel(context.Background()) + db := &DB{ + connector: c, + openerCh: make(chan struct{}, connectionRequestQueueSize), + stop: cancel, + } + go db.connectionOpener(ctx) + return 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. + +### Anti-pattern + +```go +// DON'T: Start goroutines without shutdown mechanism +go func() { + for { + processWork() // runs forever, no way to stop + } +}() +``` + +--- + +## 11. noCopy — Preventing Value Copies + +### Source: `src/sync/cond.go:120-126` + +```go +// src/sync/cond.go:120-126 +type noCopy struct{} + +// Lock is a no-op used by -copylocks checker from `go vet`. +func (*noCopy) Lock() {} +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. + +### Anti-pattern + +```go +// DON'T: Pass sync types by value +func doWork(wg sync.WaitGroup) { // copies! + defer wg.Done() // operates on copy, not original +} + +// DO: Pass by pointer +func doWork(wg *sync.WaitGroup) { + defer wg.Done() +} +``` + +--- + +## Summary: Concurrency Decision Guide + +| Need | Use | +|------|-----| +| Protect shared state | `sync.Mutex` + `defer Unlock()` | +| One-time initialization | `sync.Once` | +| Wait for N goroutines | `sync.WaitGroup` (prefer `.Go()` in 1.25+) | +| Reduce allocation pressure | `sync.Pool` (not for connections!) | +| Signal completion/cancellation | `chan struct{}` + `close()` | +| Deadline/timeout propagation | `context.WithTimeout` / `context.WithCancel` | +| Backpressure between producer/consumer | Unbuffered channels | +| Long-lived background worker | Goroutine + context cancellation | +| Prevent struct copying | Embed `noCopy` field | diff --git a/patterns/error-handling.md b/patterns/error-handling.md new file mode 100644 index 0000000..9fe9719 --- /dev/null +++ b/patterns/error-handling.md @@ -0,0 +1,519 @@ +# Go Error Handling Patterns + +Patterns extracted from the Go standard library source code. + +--- + +## 1. Sentinel Errors + +### Source: `src/io/io.go:40-43` (EOF), `src/errors/errors.go:81-83` (ErrUnsupported) + +```go +// src/io/io.go:40-43 +// EOF is the error returned by Read when no more input is available. +// (Read must return EOF itself, not an error wrapping EOF, +// because callers will test for EOF using ==.) +var EOF = errors.New("EOF") + +// src/io/io.go:47-49 +var ErrUnexpectedEOF = errors.New("unexpected EOF") +``` + +```go +// src/errors/errors.go:81-83 +var ErrUnsupported = New("unsupported operation") +``` + +### Why + +Sentinel errors are package-level values that represent specific, well-known error conditions. They enable callers to test for specific failures: + +```go +if err == io.EOF { + // end of input — not an error, just done +} +``` + +**Critical rule from io.EOF's doc comment**: Read must return EOF itself, **not an error wrapping EOF**, because callers test for it with `==`. This is the distinction between sentinel errors (identity-checked) and wrapped errors (tree-checked). + +### Anti-pattern + +```go +// DON'T: Use string matching +if err.Error() == "EOF" { ... } // fragile, not guaranteed + +// DON'T: Return a new error each time for sentinel conditions +func Read() error { + return errors.New("EOF") // every call creates a new value, can't compare with == +} +``` + +--- + +## 2. errors.New — Minimal Error Construction + +### Source: `src/errors/errors.go:62-69` + +```go +// src/errors/errors.go:62-64 +func New(text string) error { + return &errorString{text} +} + +// src/errors/errors.go:66-69 +type errorString struct { + s string +} + +func (e *errorString) Error() string { + return e.s +} +``` + +### Why + +`errors.New` returns a pointer to a private struct. Each call creates a **distinct value** even with identical text — this is intentional for sentinel errors. Two calls to `errors.New("foo")` produce different errors (`!=`). + +The `error` interface itself is the smallest possible: +```go +type error interface { + Error() string +} +``` + +### Anti-pattern + +```go +// DON'T: Export the error type +type MyError string // callers can create values that accidentally == your sentinels + +// DON'T: Use plain strings as errors +func doThing() error { + return "something failed" // doesn't implement error interface +} +``` + +--- + +## 3. Error Wrapping with fmt.Errorf and %w + +### Source: `src/fmt/errors.go:13-23`, `src/fmt/errors.go:70-80` + +```go +// src/fmt/errors.go:13-23 +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// +// If the format specifier includes a %w verb with an error operand, +// the returned error will implement an Unwrap method returning the operand. +// If there is more than one %w verb, the returned error will implement an +// Unwrap method returning a []error containing all the %w operands. +func Errorf(format string, a ...any) (err error) { ... } + +// src/fmt/errors.go:70-80 +type wrapError struct { + msg string + err error +} + +func (e *wrapError) Error() string { + return e.msg +} + +func (e *wrapError) Unwrap() error { + return e.err +} +``` + +### Why + +`%w` creates an error chain: the returned error wraps the original. `errors.Is` and `errors.As` walk this chain. Use `%w` when callers should be able to inspect the underlying cause. + +```go +// Wraps: callers can detect the original error +return fmt.Errorf("open config: %w", err) + +// Does NOT wrap: hides the original error +return fmt.Errorf("open config: %v", err) +``` + +### When to use %w vs %v + +- **%w**: When the wrapped error is part of your API contract. Callers can depend on it. +- **%v**: When you want to include the error text but NOT let callers depend on the underlying type. Use for implementation details. + +### Anti-pattern + +```go +// DON'T: Lose the original error +return errors.New("failed to open config") // original error vanished + +// DON'T: Wrap errors that aren't part of your contract with %w +return fmt.Errorf("internal: %w", internalErr) // now callers depend on internalErr's type +``` + +--- + +## 4. errors.Is — Checking Error Identity Through Chains + +### Source: `src/errors/wrap.go:30-44` + +```go +// src/errors/wrap.go:30-44 +func Is(err, target error) bool { + if err == nil || target == nil { + return err == target + } + isComparable := reflectlite.TypeOf(target).Comparable() + return is(err, target, isComparable) +} + +func is(err, target error, targetComparable bool) bool { + for { + if targetComparable && err == target { + return true + } + if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { + return true + } + switch x := err.(type) { + case interface{ Unwrap() error }: + err = x.Unwrap() + if err == nil { + return false + } + case interface{ Unwrap() []error }: + for _, err := range x.Unwrap() { + if is(err, target, targetComparable) { + return true + } + } + return false + default: + return false + } + } +} +``` + +### Why + +`errors.Is` walks the entire error tree (depth-first). It checks: +1. Direct equality (`err == target`) +2. Custom `Is(error) bool` method on the error +3. Then unwraps and recurses + +This means wrapped errors are transparent: +```go +err := fmt.Errorf("config: %w", os.ErrNotExist) +errors.Is(err, os.ErrNotExist) // true! walks the chain +``` + +### Anti-pattern + +```go +// DON'T: Use == directly on potentially-wrapped errors +if err == os.ErrNotExist { ... } // fails if err wraps ErrNotExist + +// DO: Use errors.Is +if errors.Is(err, os.ErrNotExist) { ... } // works through wrapping +``` + +--- + +## 5. errors.As — Extracting Error Types Through Chains + +### Source: `src/errors/wrap.go:96-120` + +```go +// src/errors/wrap.go:96-120 +func As(err error, target any) bool { + if err == nil { + return false + } + // ... validation ... + targetType := typ.Elem() + return as(err, target, val, targetType) +} +``` + +The `as` function (line 121+) walks the tree checking `AssignableTo` and custom `As(any) bool` methods. + +### Why + +Extract specific error types from wrapped chains: + +```go +var pathErr *fs.PathError +if errors.As(err, &pathErr) { + fmt.Println("failed path:", pathErr.Path) +} +``` + +### Go 1.24+: errors.AsType (generic version) + +From `src/errors/errors.go:48-56` doc: +```go +if perr, ok := errors.AsType[*fs.PathError](err); ok { + fmt.Println(perr.Path) +} +``` + +### Anti-pattern + +```go +// DON'T: Type-assert directly on potentially-wrapped errors +if pathErr, ok := err.(*fs.PathError); ok { ... } // fails if wrapped + +// DO: Use errors.As +var pathErr *fs.PathError +if errors.As(err, &pathErr) { ... } // works through wrapping +``` + +--- + +## 6. errors.Join — Multi-Error Aggregation + +### Source: `src/errors/join.go:20-39` + +```go +// src/errors/join.go:20-39 +func Join(errs ...error) error { + n := 0 + for _, err := range errs { + if err != nil { + n++ + } + } + if n == 0 { + return nil + } + e := &joinError{ + errs: make([]error, 0, n), + } + for _, err := range errs { + if err != nil { + e.errs = append(e.errs, err) + } + } + return e +} +``` + +The `joinError` type implements `Unwrap() []error`, making both `Is` and `As` traverse correctly. + +### Why + +For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error. + +```go +var errs []error +errs = append(errs, closeDB()) +errs = append(errs, closeCache()) +return errors.Join(errs...) // nil if all nil +``` + +### Anti-pattern + +```go +// DON'T: Return only the last error +var lastErr error +for _, r := range resources { + if err := r.Close(); err != nil { + lastErr = err // silently loses previous errors + } +} +return lastErr +``` + +--- + +## 7. Custom Is() Method — Equivalence Classes + +### Source: `src/errors/wrap.go:42-44` (doc comment), `src/context/context.go:177-179` + +From the `errors.Is` doc: +```go +// An error type might provide an Is method so it can be treated as +// equivalent to an existing error. For example, if MyError defines +// +// func (m MyError) Is(target error) bool { return target == fs.ErrExist } +// +// then Is(MyError{}, fs.ErrExist) returns true. +``` + +Real example from context: +```go +// src/context/context.go:177-179 +type deadlineExceededError struct{} + +func (deadlineExceededError) Error() string { return "context deadline exceeded" } +func (deadlineExceededError) Timeout() bool { return true } +func (deadlineExceededError) Temporary() bool { return true } +``` + +### Why + +Custom `Is` methods let you define error equivalence beyond pointer identity. A `syscall.Errno` can match `fs.ErrExist` through its `Is` method, bridging OS-specific error codes to portable sentinel errors. + +### Anti-pattern + +```go +// DON'T: Make Is() too broad +func (e MyError) Is(target error) bool { + return true // matches everything — defeats the purpose +} +``` + +--- + +## 8. Error Wrapping in Custom Types (Unwrap pattern) + +### Source: `src/encoding/json/encode.go:276-293` + +```go +// src/encoding/json/encode.go:276-282 +type MarshalerError struct { + Type reflect.Type + Err error + sourceFunc string +} + +// src/encoding/json/encode.go:293 +func (e *MarshalerError) Unwrap() error { return e.Err } +``` + +### Why + +Custom error types carry structured data (which type failed, which function) while still participating in the error chain via `Unwrap()`. Callers can use `errors.As` to extract the `MarshalerError` AND use `errors.Is` to check the underlying cause. + +### Pattern Template + +```go +type OpError struct { + Op string + Path string + Err error +} + +func (e *OpError) Error() string { + return e.Op + " " + e.Path + ": " + e.Err.Error() +} + +func (e *OpError) Unwrap() error { + return e.Err +} +``` + +### Anti-pattern + +```go +// DON'T: Store error as string +type MyError struct { + Message string // lost the original error! +} + +// DON'T: Forget to implement Unwrap +type MyError struct { + Err error // has the error but errors.Is can't traverse it +} +func (e *MyError) Error() string { return e.Err.Error() } +// Missing: func (e *MyError) Unwrap() error { return e.Err } +``` + +--- + +## 9. ErrUnsupported — Feature Detection via Errors + +### Source: `src/errors/errors.go:76-83` + +```go +// src/errors/errors.go:76-83 +// ErrUnsupported indicates that a requested operation cannot be performed, +// because it is unsupported. +// +// Functions and methods should not return this error but should instead +// return an error including appropriate context that satisfies +// +// errors.Is(err, errors.ErrUnsupported) +// +// either by directly wrapping ErrUnsupported or by implementing an Is method. +var ErrUnsupported = New("unsupported operation") +``` + +### Why + +This pattern separates "what happened" (detailed context) from "what kind of failure" (sentinel identity). Return a rich error that *wraps* or *matches* the sentinel: + +```go +return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported) +``` + +### Anti-pattern + +```go +// DON'T: Return the sentinel directly without context +return errors.ErrUnsupported // no info about what operation or why +``` + +--- + +## 10. Error String Conventions + +### Source: `src/net/http/server.go:39-56` + +```go +// src/net/http/server.go:39-56 +var ( + ErrHijacked = errors.New("http: connection has been hijacked") + ErrContentLength = errors.New("http: wrote more than the declared Content-Length") +) +``` + +### Convention: Error String Format + +``` +package: description +``` + +- Lowercase (no capital first letter) +- No trailing punctuation +- Package prefix for disambiguation + +### Anti-pattern + +```go +// DON'T: Capitalize error strings +errors.New("Connection has been hijacked") + +// 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 +``` + +--- + +## Summary: Error Handling Decision Tree + +``` +Is this a specific, well-known condition? +├── YES → Sentinel error (package-level var) +│ └── Should callers detect it? → errors.Is +└── NO → Is there structured info to convey? + ├── YES → Custom error type with Unwrap() + │ └── Should callers extract it? → errors.As + └── NO → fmt.Errorf with %w (wraps) or %v (doesn't wrap) +``` + +| When to... | Use | +|---|---| +| Create a well-known error condition | `var ErrFoo = errors.New("pkg: foo")` | +| Add context while preserving cause | `fmt.Errorf("doing X: %w", err)` | +| Add context, hide internal cause | `fmt.Errorf("doing X: %v", err)` | +| Check for a specific condition | `errors.Is(err, ErrFoo)` | +| Extract structured error data | `errors.As(err, &target)` | +| Aggregate multiple errors | `errors.Join(err1, err2)` | +| Make custom types traversable | Implement `Unwrap() error` | +| Define error equivalence | Implement `Is(error) bool` | diff --git a/patterns/package-design.md b/patterns/package-design.md new file mode 100644 index 0000000..c359964 --- /dev/null +++ b/patterns/package-design.md @@ -0,0 +1,467 @@ +# Go Package Design Patterns + +Patterns extracted from the Go standard library source code. + +--- + +## 1. Package-Level Documentation + +### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57` + +```go +// src/io/io.go:5-13 +// Package io provides basic interfaces to I/O primitives. +// Its primary job is to wrap existing implementations of such primitives, +// such as those in package os, into shared public interfaces that +// abstract the functionality, plus some other related primitives. +// +// Because these interfaces and primitives wrap lower-level operations with +// various implementations, unless otherwise informed clients should not +// assume they are safe for parallel execution. +package io +``` + +```go +// src/sync/mutex.go:5-11 +// Package sync provides basic synchronization primitives such as mutual +// exclusion locks. Other than the Once and WaitGroup types, most are intended +// for use by low-level library routines. Higher-level synchronization is +// better done via channels and communication. +// +// Values containing the types defined in this package should not be copied. +package sync +``` + +### Why + +The package comment: +1. **States the purpose** in one sentence +2. **Establishes contracts** (not safe for parallel execution, values must not be copied) +3. **Guides users** toward correct usage (prefer channels over mutexes) +4. **Appears before `package` keyword** — becomes `go doc` output + +### Convention + +- First sentence: `"Package X does Y."` or `"Package X provides Y."` +- For multi-file packages, put the package comment in `doc.go` or the primary file + +### Anti-pattern + +```go +// DON'T: No package comment +package myutil + +// DON'T: Restate the obvious +// Package http provides HTTP stuff. +package http +``` + +--- + +## 2. Package Naming + +### Source: All stdlib packages follow these conventions + +**Stdlib examples:** +- `io` — not `ioutil`, not `ioutils` +- `fmt` — not `format`, not `formatting` +- `sync` — not `synchronization` +- `net/http` — not `net/httpserver` +- `encoding/json` — not `encoding/jsonparser` +- `context` — not `ctx` or `contexts` + +### Why + +Go package names are **short, lowercase, no underscores or mixedCaps**. The package name is part of every qualified identifier: + +```go +// Good: package name provides context +http.Server // not http.HTTPServer +json.Encoder // not json.JSONEncoder +context.Context // the type IS the context +``` + +### Anti-pattern + +```go +// DON'T: Stutter +package http +type HTTPServer struct{} // http.HTTPServer — redundant + +// DON'T: Utility package names +package utils // what does it DO? +package helpers // grab bag, no cohesion +package common // everything ends up here +``` + +--- + +## 3. internal/ Packages — Restricting Visibility + +### Source: `src/net/http/internal/`, `src/encoding/json/internal.go` + +``` +src/net/http/internal/ +├── ascii/ +├── chunked.go +├── http2/ +├── httpcommon/ +├── 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. + +- `net/http/internal/ascii` → importable by `net/http` and children +- NOT importable by `net/url` or any other package + +### Anti-pattern + +```go +// DON'T: Export implementation details +package mylib +func HelperThatOnlyIUse() {} // pollutes API surface + +// DO: Move to internal/ +``` + +--- + +## 4. Export Rules — The Capital Letter Boundary + +### Source: `src/io/io.go` — exported vs unexported + +```go +// src/io/io.go +var EOF = errors.New("EOF") // exported: uppercase +var errInvalidWrite = errors.New(...) // unexported: lowercase + +type teeReader struct { // unexported type + r Reader + w Writer +} + +func TeeReader(r Reader, w Writer) Reader { // exported constructor + return &teeReader{r, w} +} +``` + +### Why + +`teeReader` is unexported because: +1. Users don't need to know its implementation +2. The return type is `Reader` (interface) — maximum flexibility +3. The struct's fields can change without breaking anyone + +### Anti-pattern + +```go +// DON'T: Export everything "just in case" +type Parser struct { + Input string // should this be settable? + buffer []byte // internal state + pos int +} +``` + +--- + +## 5. init() Functions — Use Sparingly + +### Source: `src/net/http/http2.go:37` + +```go +// src/net/http/http2.go:37 +func init() { + // register HTTP/2 protocol implementation +} +``` + +### Why + +The stdlib uses `init()` for: +- **Driver registration** (database drivers register via init) +- **Protocol negotiation** (HTTP/2 registers its handler) + +### Rules + +1. Should have no side effects beyond registration +2. No errors possible (can't return error from init) +3. Keep them short +4. Prefer explicit initialization in `main()` when possible + +### Anti-pattern + +```go +// DON'T: Do heavy work in init +func init() { + db = connectToDatabase() // fails silently, crashes later + cache = loadGigabyteFile() // blocks startup +} + +// DO: Prefer explicit setup in main() +func main() { + db, err := connectToDatabase() + if err != nil { + log.Fatal(err) + } +} +``` + +--- + +## 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: + +```go +// The pattern (idiom from Rob Pike/Dave Cheney): +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { + s.timeout = d + } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{addr: addr, timeout: 30 * time.Second} + for _, opt := range opts { + opt(s) + } + return s +} +``` + +### What stdlib uses: Config structs + +```go +// net/http — struct literal configuration +srv := &http.Server{ + Addr: ":8080", + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + Handler: mux, +} +``` + +### When to use which + +| Approach | When | +|----------|------| +| Config struct | Few options, all data (stdlib preference) | +| Functional options | Many options, some involve behavior, public API stability | + +--- + +## 7. Constructor Pattern — NewX Functions + +### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836` + +```go +// src/net/http/server.go:2639 +func NewServeMux() *ServeMux { + return new(ServeMux) +} + +// src/database/sql/sql.go:836-843 +func OpenDB(c driver.Connector) *DB { + ctx, cancel := context.WithCancel(context.Background()) + db := &DB{ + connector: c, + openerCh: make(chan struct{}, connectionRequestQueueSize), + stop: cancel, + } + go db.connectionOpener(ctx) + return 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`) + +### Anti-pattern + +```go +// DON'T: Constructor that returns interface +func NewWriter() io.Writer { return &myWriter{} } // hides methods + +// DON'T: Require constructor when zero value works +// var b bytes.Buffer ← just works +``` + +--- + +## 8. Package Organization — One Concern Per Package + +### Source: Standard library structure + +``` +src/ +├── io/ # I/O interfaces + helpers +├── os/ # OS operations +├── net/ # network primitives +│ ├── http/ # HTTP protocol +│ └── url/ # URL parsing +├── encoding/ +│ ├── json/ # JSON codec +│ └── xml/ # XML codec +├── database/ +│ └── sql/ # SQL abstraction +│ └── driver/ # SPI for drivers +└── context/ # cancellation propagation +``` + +### Why + +Each package has a single, clear responsibility. Packages communicate through interfaces, not shared state. + +### Anti-pattern + +```go +// DON'T: Package per type (50 packages with 1 file each) +package user +package order +package payment + +// DON'T: Circular dependencies +package a imports package b +package b imports package a // compile error +``` + +--- + +## 9. API Layering — User vs Implementor (database/sql) + +### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go` + +**User-facing (database/sql):** +```go +db, _ := sql.Open("postgres", connStr) +rows, _ := db.QueryContext(ctx, "SELECT ...") +``` + +**Driver-facing (database/sql/driver):** +```go +type Driver interface { + Open(name string) (Conn, error) +} +type Conn interface { + Prepare(query string) (Stmt, error) + Close() error + Begin() (Tx, error) +} +``` + +### 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. + +--- + +## 10. Context Key Pattern — Type-Safe Context Values + +### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252` + +```go +// src/context/context.go:132-164 (from doc) +// package user +// +// type key int +// var userKey key +// +// func NewContext(ctx context.Context, u *User) context.Context { +// return context.WithValue(ctx, userKey, u) +// } +// +// func FromContext(ctx context.Context) (*User, bool) { +// u, ok := ctx.Value(userKey).(*User) +// return u, ok +// } +``` + +```go +// src/net/http/server.go:244-252 +var ( + ServerContextKey = &contextKey{"http-server"} + LocalAddrContextKey = &contextKey{"local-addr"} +) + +type contextKey struct { + name string +} +``` + +### Why + +- **Unexported key type** prevents other packages from accessing your values +- **Type-safe accessors** avoid repeated type assertions +- **Pointer-based keys** guarantee uniqueness + +### Anti-pattern + +```go +// DON'T: Use string keys (collision risk) +ctx = context.WithValue(ctx, "user", user) + +// 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 | +| Constructors | `NewX()` → `*T`; prefer usable zero values | +| Organization | One concern per package | +| API layers | Separate user from implementor (SPI) | +| Context values | Unexported key type + typed accessors | +| Configuration | Struct literals or functional options | diff --git a/patterns/testing-advanced.md b/patterns/testing-advanced.md new file mode 100644 index 0000000..ecaba8c --- /dev/null +++ b/patterns/testing-advanced.md @@ -0,0 +1,586 @@ +# Advanced Go Testing Patterns + +Patterns extracted from the Go standard library (`src/net/http/`, `src/encoding/json/`, `src/testing/`) and Kubernetes source code. + +--- + +## 1. Table-Driven Tests + +The canonical Go test style. Every Go stdlib test file uses this pattern. + +### Pattern Name: Anonymous Struct Test Table + +**Source:** `/tmp/go-src/src/net/http/header_test.go` lines 17-108 + +**What they do:** Define test cases as a slice of anonymous structs, iterate with a range loop. + +**Why:** Eliminates repetition, makes adding cases trivial, keeps the assertion logic in one place. Every test case gets the same verification path — no "special" cases hidden in different code paths. + +**Anti-pattern:** Writing individual assertions for each case, or copy-pasting test functions that differ by one input. + +**Code example (stdlib):** +```go +var headerWriteTests = []struct { + h Header + exclude map[string]bool + expected string +}{ + {Header{}, nil, ""}, + { + Header{ + "Content-Type": {"text/html; charset=UTF-8"}, + "Content-Length": {"0"}, + }, + nil, + "Content-Length: 0\r\nContent-Type: text/html; charset=UTF-8\r\n", + }, + // ... more cases +} + +func TestHeaderWrite(t *testing.T) { + var buf strings.Builder + for i, test := range headerWriteTests { + test.h.WriteSubset(&buf, test.exclude) + if buf.String() != test.expected { + t.Errorf("#%d:\n got: %q\nwant: %q", i, buf.String(), test.expected) + } + buf.Reset() + } +} +``` + +--- + +### Pattern Name: Named Table Tests with t.Run (Subtests) + +**Source:** `/tmp/go-src/src/encoding/json/encode_test.go` lines 285-320, `/tmp/go-src/src/encoding/json/scanner_test.go` lines 30-50 + +**What they do:** Combine table-driven tests with `t.Run` for named subtests. Use a `CaseName` struct that captures file/line for error reporting. + +**Why:** Each case gets its own subtest name — visible in `go test -v`, filterable with `-run`, and individually re-runnable. The `CaseName`/`Where` pattern provides precise file:line for failures even in large test tables. + +**Anti-pattern:** Using index-only identification (hard to find which case failed), or creating separate `TestFoo_Case1`, `TestFoo_Case2` functions. + +**Code example (stdlib):** +```go +func TestValid(t *testing.T) { + tests := []struct { + CaseName + data string + ok bool + }{ + {Name(""), `foo`, false}, + {Name(""), `}{`, false}, + {Name(""), `{}`, true}, + {Name("StringDoubleEscapes"), `{"foo":"bar"}`, true}, + } + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + if ok := Valid([]byte(tt.data)); ok != tt.ok { + t.Errorf("%s: Valid(`%s`) = %v, want %v", tt.Where, tt.data, ok, tt.ok) + } + }) + } +} +``` + +--- + +### Pattern Name: CaseName with Caller Position Tracking + +**Source:** `/tmp/go-src/src/encoding/json/internal/jsontest/testcase.go` lines 18-37 + +**What they do:** Create a helper type that captures the caller's file:line at the point of test case declaration, so error messages point back to the exact test case definition. + +**Why:** In a 1000-entry test table, `t.Errorf` points to the assertion line (same for all cases). CaseName makes failures point to the case definition. + +**Code example (stdlib):** +```go +type CaseName struct { + Name string + Where CasePos +} + +func Name(s string) (c CaseName) { + c.Name = s + runtime.Callers(2, c.Where.pc[:]) + return c +} + +type CasePos struct{ pc [1]uintptr } + +func (pos CasePos) String() string { + frames := runtime.CallersFrames(pos.pc[:]) + frame, _ := frames.Next() + return fmt.Sprintf("%s:%d", path.Base(frame.File), frame.Line) +} +``` + +--- + +## 2. Test Helper Patterns + +### Pattern Name: t.Helper() for Clean Stack Traces + +**Source:** `/tmp/go-src/src/testing/testing.go` lines 1415-1435 + +**What they do:** Call `t.Helper()` as the first line in any test utility function. This marks the function as a helper, so test failure messages report the caller's line instead of the helper's line. + +**Why:** Without `t.Helper()`, every failure in a helper function points to the helper itself, not the test case that triggered the failure. Makes debugging test failures require reading the full stack. + +**Anti-pattern:** Writing test utilities that call `t.Fatal`/`t.Error` without marking themselves as helpers. + +**Code example (stdlib):** +```go +// From net/http/clientserver_test.go lines 100-131 +func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) { + t.Helper() + modes := []testMode{http1Mode, http2Mode, http3Mode} + parallel := true + for _, opt := range opts { + switch opt := opt.(type) { + case []testMode: + modes = opt + case testNotParallelOpt: + parallel = false + default: + t.Fatalf("unknown option type %T", opt) + } + } + // ... + for _, mode := range modes { + t.Run(string(mode), func(t T) { + t.Helper() + // ... + f(t, mode) + }) + } +} +``` + +--- + +### Pattern Name: *testing.T as First Argument to Helpers + +**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 4555-4580 + +**What they do:** Pass `*testing.T` (or `testing.TB`) as the first argument to test helper functions, making the dependency on the test context explicit. + +**Why:** The test object provides `Fatal`, `Error`, `Log`, `Helper`, `Cleanup` — everything a helper needs for reporting. Accepting it as a parameter (rather than capturing it in a closure) makes helpers reusable across tests. + +**Code example (stdlib):** +```go +mustGet := func(url string, headers ...string) { + t.Helper() + req, err := NewRequest("GET", url, nil) + if err != nil { + t.Fatal(err) + } + for len(headers) > 0 { + req.Header.Add(headers[0], headers[1]) + headers = headers[2:] + } + res, err := c.Do(req) + if err != nil { + t.Errorf("Error fetching %s: %v", url, err) + return + } + _, err = io.ReadAll(res.Body) + defer res.Body.Close() +} +``` + +--- + +## 3. t.Cleanup vs defer + +### Pattern Name: t.Cleanup for Test-Scoped Resources + +**Source:** `/tmp/go-src/src/testing/testing.go` lines 1439-1468, `/tmp/go-src/src/net/http/clientserver_test.go` lines 120-127 + +**What they do:** Use `t.Cleanup(fn)` instead of `defer` for resource cleanup in tests. + +**Why:** +1. `defer` runs at the end of the *function*, not the *test*. In subtests launched with `t.Run`, a `defer` in a helper function runs when the helper returns — not when the subtest completes. +2. `t.Cleanup` runs after the test AND all its subtests finish — guaranteeing resources are available for the full test lifetime. +3. `t.Cleanup` is called in reverse order (LIFO), matching `defer` semantics but scoped to the test. + +**Anti-pattern:** Using `defer` for cleanup in test setup functions that return before the test finishes, or in subtests where timing matters. + +**Code example (stdlib):** +```go +// From net/http/clientserver_test.go +func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) { + // ... + for _, mode := range modes { + t.Run(string(mode), func(t T) { + t.Cleanup(func() { + afterTest(t) // Goroutine leak detection — runs AFTER subtest body completes + }) + f(t, mode) + }) + } +} +``` + +--- + +## 4. testdata/ Directory Pattern + +### Pattern Name: testdata/ for Test Fixtures + +**Source:** `/tmp/go-src/src/net/http/testdata/` (contains `file`, `index.html`, `style.css`), `/tmp/go-src/src/net/http/fs_test.go` line 38 + +**What they do:** Store test fixtures in a `testdata/` directory adjacent to the test files. Reference them with relative paths like `"testdata/file"`. + +**Why:** +1. `go build` ignores `testdata/` directories — they never end up in production binaries. +2. `go test` runs with the package directory as CWD — relative paths to `testdata/` work reliably. +3. Fixtures are version-controlled alongside the code they test. +4. Separates test data from test logic. + +**Anti-pattern:** Embedding large test fixtures as string literals in test files, or referencing absolute paths. + +**Code example (stdlib):** +```go +// From net/http/fs_test.go line 38 +const testFile = "testdata/file" + +// Usage in test: +ServeFile(w, r, "testdata/file") +``` + +--- + +## 5. Golden File Testing + +### Pattern Name: Golden Files with -update Flag + +**Source:** `/tmp/go-src/src/cmd/gofmt/gofmt_test.go` lines 18, 113-138 + +**What they do:** Compare test output against `.golden` files. Provide a `-update` flag that regenerates golden files from current output when behavior intentionally changes. + +**Why:** +1. Tests complex output (formatted code, generated HTML, serialized data) without embedding it in test code. +2. The `-update` flag makes intentional changes easy: run `go test -update`, review the diff, commit. +3. Golden files serve as documentation of expected behavior. +4. Reviewers can see exactly what output changed in diffs. + +**Anti-pattern:** Comparing against inline expected strings that span 50+ lines, or manually constructing expected output. + +**Code example (stdlib):** +```go +var update = flag.Bool("update", false, "update .golden files") + +func runTest(t *testing.T, in, out string) { + // ... produce actual output ... + + expected, err := os.ReadFile(out) + if err != nil { + t.Error(err) + return + } + + if got := buf.Bytes(); !bytes.Equal(got, expected) { + if *update { + if in != out { + if err := os.WriteFile(out, got, 0666); err != nil { + t.Error(err) + } + return + } + } + t.Errorf("(gofmt %s) != %s\n%s", in, out, + diff.Diff("expected", expected, "got", got)) + } +} + +func TestRewrite(t *testing.T) { + match, _ := filepath.Glob("testdata/*.input") + for _, in := range match { + name := filepath.Base(in) + t.Run(name, func(t *testing.T) { + out := in[:len(in)-len(".input")] + ".golden" + runTest(t, in, out) + }) + } +} +``` + +--- + +## 6. httptest Patterns + +### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers + +**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 387-393 + +**What they do:** Use `httptest.NewRecorder()` to test HTTP handlers without starting a server. Captures status code, headers, and body. + +**Why:** Fast, no network, no port allocation, no goroutines. Perfect for unit testing individual handlers in isolation. + +**Anti-pattern:** Spinning up a full server to test handler logic that doesn't need networking. + +**Code example (stdlib):** +```go +func TestServeMuxHandler(t *testing.T) { + mux := NewServeMux() + for _, e := range serveMuxRegister { + mux.Handle(e.pattern, e.h) + } + for _, tt := range serveMuxTests { + r := &Request{Method: tt.method, Host: tt.host, URL: &url.URL{Path: tt.path}} + h, pattern := mux.Handler(r) + rr := httptest.NewRecorder() + h.ServeHTTP(rr, r) + if pattern != tt.pattern || rr.Code != tt.code { + t.Errorf("%s %s %s = %d, %q, want %d, %q", + tt.method, tt.host, tt.path, rr.Code, pattern, tt.code, tt.pattern) + } + } +} +``` + +--- + +### Pattern Name: httptest.NewServer for Integration-Style Tests + +**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 203-280 + +**What they do:** Use `httptest.NewServer` / `httptest.NewUnstartedServer` for end-to-end HTTP testing with a real TCP listener on localhost. + +**Why:** Tests the full HTTP stack including transport, TLS, connection pooling, timeouts. The `clientServerTest` helper in the stdlib runs each test across HTTP/1.1, HTTP/2, and HTTP/3 modes. + +**Code example (stdlib):** +```go +func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *clientServerTest { + cst := &clientServerTest{t: t, h2: mode == http2Mode, h: h} + cst.ts = httptest.NewUnstartedServer(h) + // ... configure based on mode ... + switch mode { + case http1Mode: + cst.ts.Start() + case http2Mode: + cst.ts.EnableHTTP2 = true + cst.ts.StartTLS() + } + cst.c = cst.ts.Client() + t.Cleanup(cst.close) + return cst +} +``` + +--- + +## 7. Benchmark Patterns + +### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes + +**Source:** `/tmp/go-src/src/encoding/json/bench_test.go` lines 85-101 + +**What they do:** Combine `b.ReportAllocs()` for allocation reporting, `b.RunParallel` for concurrent benchmarks, and `b.SetBytes` for throughput metrics. + +**Why:** +- `b.ReportAllocs()` shows allocations/op — critical for hot paths. +- `b.RunParallel` measures performance under contention (real-world server behavior). +- `b.SetBytes` converts to MB/s throughput — meaningful for serialization benchmarks. + +**Anti-pattern:** Benchmarks that only measure wall time without allocation tracking, or sequential benchmarks for concurrent code. + +**Code example (stdlib):** +```go +func BenchmarkCodeEncoder(b *testing.B) { + b.ReportAllocs() + if codeJSON == nil { + b.StopTimer() + codeInit() + b.StartTimer() + } + b.RunParallel(func(pb *testing.PB) { + enc := NewEncoder(io.Discard) + for pb.Next() { + if err := enc.Encode(&codeStruct); err != nil { + b.Fatalf("Encode error: %v", err) + } + } + }) + b.SetBytes(int64(len(codeJSON))) +} +``` + +--- + +## 8. Integration Test Separation + +### Pattern Name: testing.Short() for Expensive Tests + +**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 800, 1000, 2212, 2581 + +**What they do:** Skip slow/flaky/network-dependent tests with `testing.Short()`. The Go CI runs with `-short` in fast mode, full tests in thorough mode. + +**Why:** Fast feedback loop for development (`go test -short`), full validation in CI. No custom build tags needed. + +**Anti-pattern:** Separate `_integration_test.go` files with build tags (Go stdlib doesn't do this), or always-slow tests that can't be skipped. + +**Code example (stdlib):** +```go +func TestServerTimeouts(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + // ... expensive test with real timeouts ... +} +``` + +--- + +## 9. No Assertion Libraries in Stdlib + +### Pattern Name: Plain if/t.Errorf Over Assertion Frameworks + +**Source:** Every test file in `/tmp/go-src/src/` (zero imports of `testify`, `gomega`, or any assertion library) + +**What they do:** Use plain Go: `if got != want { t.Errorf(...) }`. Never import assertion libraries. + +**Why:** +1. No implicit control flow — `t.Errorf` continues execution, so you see ALL failures at once. +2. No magic — the test reads like regular Go code. +3. Error messages are custom-crafted for each assertion, providing context that generic `assert.Equal` cannot. +4. One less dependency. + +**Anti-pattern (Kubernetes uses this, stdlib does NOT):** +```go +// Kubernetes style (not stdlib): +assert.Equal(t, expected, actual) +require.NoError(t, err) +``` + +**Stdlib style:** +```go +if got := v.Elem().Interface(); !reflect.DeepEqual(got, tt.out) { + t.Fatalf("%s: Decode:\n\tgot: %#v\n\twant: %#v", tt.Where, got, tt.out) +} +``` + +--- + +## 10. Goroutine Leak Detection + +### Pattern Name: TestMain + afterTest Goroutine Checking + +**Source:** `/tmp/go-src/src/net/http/main_test.go` (entire file) + +**What they do:** `TestMain` runs the test suite and checks for leaked goroutines after all tests complete. `afterTest` checks for goroutine leaks after each individual test. + +**Why:** HTTP code spawns goroutines for connections, background reads, etc. Leaked goroutines indicate resource leaks (connections not closed, servers not shut down). Catching them prevents production OOMs. + +**Code example (stdlib):** +```go +func TestMain(m *testing.M) { + v := m.Run() + if v == 0 && goroutineLeaked() { + os.Exit(1) + } + os.Exit(v) +} + +func goroutineLeaked() bool { + for i := 0; i < 5; i++ { + gs := interestingGoroutines() + if len(gs) == 0 { + return false + } + time.Sleep(100 * time.Millisecond) + } + // Report leaked goroutines + return true +} + +func afterTest(t testing.TB) { + http.DefaultTransport.(*http.Transport).CloseIdleConnections() + // Check for leaked goroutines from this specific test... +} +``` + +--- + +## 11. export_test.go Pattern + +### Pattern Name: Bridge File for Internal Testing + +**Source:** `/tmp/go-src/src/net/http/export_test.go` lines 1-50 + +**What they do:** Create an `export_test.go` file in the package itself (package `http`, not `http_test`) that exports internal symbols to external test packages. Only compiled during testing. + +**Why:** Allows `http_test` (external test package) to access internals needed for white-box testing without polluting the public API. The `_test.go` suffix means it's never included in production builds. + +**Code example (stdlib):** +```go +// export_test.go — package http (not http_test!) +package http + +var ( + DefaultUserAgent = defaultUserAgent + ExportRefererForURL = refererForURL + ExportServerNewConn = (*Server).newConn + ExportErrRequestCanceled = errRequestCanceled +) +``` + +--- + +## 12. Multi-Mode Test Runner + +### Pattern Name: Generic Test Runner Across Protocol Modes + +**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 100-134 + +**What they do:** A generic `run[T]` function that executes every client/server test in HTTP/1.1, HTTP/2, and HTTP/3 modes automatically. Tests opt into specific modes via options. + +**Why:** Ensures behavioral consistency across protocol versions. A single test function covers all modes — no duplication. Bugs in one protocol version are caught immediately. + +**Code example (stdlib):** +```go +// Test declaration (one line runs across 3 protocols): +func TestServerTimeouts(t *testing.T) { run(t, testServerTimeouts, []testMode{http1Mode}) } + +// The runner: +func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) { + t.Helper() + modes := []testMode{http1Mode, http2Mode, http3Mode} + for _, mode := range modes { + t.Run(string(mode), func(t T) { + t.Helper() + t.Cleanup(func() { afterTest(t) }) + f(t, mode) + }) + } +} +``` + +--- + +## 13. testLogWriter — Routing Server Logs to Test Output + +### Pattern Name: io.Writer Adapter for *testing.T + +**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 337-345 + +**What they do:** Implement `io.Writer` backed by `t.Logf`, so server error logs appear in test output (visible with `-v`, suppressed otherwise). + +**Why:** Server logs are crucial for debugging test failures but shouldn't clutter passing output. `t.Log` gives you both: silent on pass, verbose on fail. + +**Code example (stdlib):** +```go +type testLogWriter struct { + t testing.TB +} + +func (w testLogWriter) Write(b []byte) (int, error) { + w.t.Logf("server log: %v", strings.TrimSpace(string(b))) + return len(b), nil +} + +// Usage: +cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0) +```