docs: idiomatic Go patterns from stdlib + Kubernetes with source citations

This commit is contained in:
Rodin
2026-04-30 11:07:33 +00:00
parent 0f1d7e4c06
commit c797178fb9
6 changed files with 2184 additions and 0 deletions
+17
View File
@@ -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.
View File
+595
View File
@@ -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 |
+519
View File
@@ -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` |
+467
View File
@@ -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 |
+586
View File
@@ -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)
```