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

This commit is contained in:
Rodin
2026-04-30 04:07:41 -07:00
commit b5d0544fd6
7 changed files with 3997 additions and 0 deletions
+707
View File
@@ -0,0 +1,707 @@
# 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()
}
// src/sync/mutex.go:64-67
func (m *Mutex) Unlock() {
m.mu.Unlock()
}
```
### 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 waiting to happen
// DON'T: Forget defer
mu.Lock()
// if this panics, the mutex stays locked forever
doSomething()
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 the comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait.
The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27).
### Idiomatic Usage
```go
var (
instance *DB
once sync.Once
)
func GetDB() *DB {
once.Do(func() {
instance = connectToDB()
})
return instance
}
```
### Anti-pattern
```go
// DON'T: Implement once yourself with a bool
var initialized bool
var mu sync.Mutex
func init() {
mu.Lock()
if !initialized {
// ... setup ...
initialized = true
}
mu.Unlock()
}
// DON'T: Call Do recursively (deadlocks)
var once sync.Once
once.Do(func() {
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
// A WaitGroup is a counting semaphore typically used to wait
// for a group of goroutines or tasks to finish.
//
// Typically, a main goroutine will start tasks, each in a new
// goroutine, by calling WaitGroup.Go and then wait for all tasks to
// complete by calling WaitGroup.Wait. For example:
//
// var wg sync.WaitGroup
// wg.Go(task1)
// 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 {
panic(x) // don't call Done — let panic propagate
}
wg.Done()
}()
f()
}()
}
```
### Why
`WaitGroup.Go` (new in Go 1.25) encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit.
### Classic Pattern (pre-Go 1.25)
```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()
// DON'T: Forget Done (Wait blocks forever)
wg.Add(1)
go func() {
process()
// forgot wg.Done()
}()
wg.Wait() // hangs
```
---
## 4. sync.Pool — Object Reuse for GC Pressure
### Source: `src/sync/pool.go:44-63`
```go
// src/sync/pool.go:44-63
// A Pool is a set of temporary objects that may be individually saved and
// retrieved.
//
// Any item stored in the Pool may be removed automatically at any time without
// notification. If the Pool holds the only reference when this happens, the
// item might be deallocated.
//
// Pool's purpose is to cache allocated but unused items for later reuse,
// relieving pressure on the garbage collector.
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. It's for reducing allocation pressure on hot paths — `fmt` uses it for print buffers, `encoding/json` for encoder state.
### Idiomatic Usage (from fmt package)
```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 at any time — use database/sql's pool instead
// DON'T: Put dirty objects back without resetting
pool.Put(buf) // still has data from last use — memory leak or data leak
```
---
## 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 {
wrMu sync.Mutex
wrCh chan []byte
rdCh chan int
once sync.Once
done chan struct{} // closed on pipe close
rerr onceError
werr onceError
}
```
### 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 per signal, true/false meaningless
// DON'T: Send to done (only works once, only one receiver)
done <- struct{}{} // only unblocks one goroutine
// DO: Close the channel (broadcasts to all)
close(done)
```
---
## 6. Context Propagation
### Source: `src/context/context.go:37-48` (rules), `src/net/http/request.go:368-380`
From the package doc:
```go
// src/context/context.go:37-48
// Programs that use Contexts should follow these rules:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
// 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.
```
### Request context in net/http:
```go
// src/net/http/request.go:368-380
func (r *Request) WithContext(ctx context.Context) *Request {
if ctx == nil {
panic("nil context")
}
r2 := new(Request)
*r2 = *r
r2.ctx = ctx
return r2
}
```
### Why
Context flows **down** the call chain, never stored in structs. `WithContext` returns a shallow copy — the original request is not mutated. This is the immutable-context pattern.
### Anti-pattern
```go
// DON'T: Store context in a struct
type Server struct {
ctx context.Context // stale context persists beyond request lifecycle
}
// DON'T: Pass nil context
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 (WithCancel/WithTimeout)
### Source: `src/context/context.go:242-249` (WithCancel), `src/net/http/server.go:4007-4050` (TimeoutHandler)
```go
// src/context/context.go:242-249
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
```
Real-world use — net/http TimeoutHandler:
```go
// src/net/http/server.go:4011-4014
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
defer cancelCtx()
r = r.WithContext(ctx)
done := make(chan struct{})
// ...
go func() {
h.handler.ServeHTTP(tw, r)
close(done)
}()
select {
case <-done:
// handler completed
case <-ctx.Done():
// timeout
}
}
```
### Why
`defer cancelCtx()` is critical — it releases resources (timers, goroutines) when the parent returns, even if the child hasn't timed out yet. The go vet tool checks for this.
### Anti-pattern
```go
// DON'T: Forget to call cancel (leaks goroutines)
ctx, _ := context.WithCancel(parent) // cancel function discarded!
// DON'T: Cancel before work starts
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
cancel() // immediately cancels — no work can happen
doWork(ctx)
```
---
## 8. Select with Done Channel
### Source: `src/context/context.go:83-100` (Done in select), `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 the current one.
### Standard Context Select Pattern
```go
// From context package doc (line 83-100)
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
```
### Anti-pattern
```go
// DON'T: Check ctx.Done() in a busy loop
for {
select {
case <-ctx.Done():
return
default:
}
// busy-spins CPU at 100%
}
```
---
## 9. Goroutine-per-Connection (net/http Server)
### Source: `src/net/http/server.go` (conceptual — the serve loop spawns goroutines per connection)
```go
// The pattern (simplified from server.go serve loop):
for {
conn, err := listener.Accept()
if err != nil {
// handle
continue
}
go srv.handleConn(conn) // one goroutine per connection
}
```
### Why
Go's goroutines are cheap (~2KB initial stack). The server doesn't need a thread pool or async/await — it spawns a goroutine per connection and lets the runtime scheduler handle multiplexing.
### Anti-pattern
```go
// DON'T: Limit yourself to a fixed thread pool for I/O-bound work
pool := make(chan struct{}, 10) // artificial limit on connections
for {
pool <- struct{}{} // blocks at 10
conn := accept()
go func() {
defer func() { <-pool }()
handle(conn)
}()
}
// Only appropriate for CPU-bound work or resource-constrained scenarios
```
---
## 10. Channel as Synchronous Pipe (io.Pipe)
### Source: `src/io/pipe.go:38-45`, `src/io/pipe.go:195-205`
```go
// src/io/pipe.go:38-45
type pipe struct {
wrMu sync.Mutex
wrCh chan []byte // writer sends data slices
rdCh chan int // reader returns bytes consumed
once sync.Once
done chan struct{}
rerr onceError
werr onceError
}
// src/io/pipe.go:195-205
func Pipe() (*PipeReader, *PipeWriter) {
pw := &PipeWriter{r: PipeReader{pipe: pipe{
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}}}
return &pw.r, pw
}
```
### Why
`io.Pipe` connects a Writer to a Reader using **unbuffered channels** — each Write blocks until the corresponding Read consumes the data. No internal buffering means backpressure is automatic. The `done` channel signals when either end closes.
### Pattern: Channel Pipeline
```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) — receivers hang on range
}()
return ch
}
```
---
## 11. database/sql Connection Opener Goroutine
### 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),
lastPut: make(map[*driverConn]string),
stop: cancel,
}
go db.connectionOpener(ctx)
return db
}
```
### Why
A dedicated background goroutine (`connectionOpener`) processes connection requests from a buffered channel. The goroutine is controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down cleanly. This is the "long-lived worker goroutine with context shutdown" pattern.
### Anti-pattern
```go
// DON'T: Start background goroutines without shutdown mechanism
go func() {
for {
processWork() // runs forever, no way to stop it
}
}()
```
---
## 12. noCopy — Preventing Value Copies at Vet Time
### 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` in a struct makes `go vet` report an error when the struct is copied. All sync primitives use this because copying a locked mutex or in-use WaitGroup is always a bug.
### Anti-pattern
```go
// DON'T: Pass sync types by value
func doWork(wg sync.WaitGroup) { // copies the WaitGroup!
defer wg.Done()
}
// 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 |
| Fan-out with results | Buffered channel + WaitGroup |
| Long-lived background worker | Goroutine + context cancellation |
| Prevent struct copying | Embed `noCopy` field |
+535
View File
@@ -0,0 +1,535 @@
# 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`:
```go
// src/errors/join.go:57 (implicit from structure)
func (e *joinError) Unwrap() []error {
return e.errs
}
```
### Why
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error. Both `Is` and `As` traverse the tree correctly.
```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. For example, a call to os.Link when using a
// file system that does not support hard links.
//
// Functions and methods should not return this error but should instead
// return an error including appropriate context that satisfies
//
// 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)
```
Callers check the sentinel; the message provides context.
### Anti-pattern
```go
// DON'T: Return the sentinel directly without context
return errors.ErrUnsupported // no info about what operation or why
```
---
## 10. Error Value Patterns in net/http
### Source: `src/net/http/server.go:39-56`
```go
// src/net/http/server.go:39-56
var (
ErrBodyNotAllowed = internal.ErrBodyNotAllowed
ErrHijacked = errors.New("http: connection has been hijacked")
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
ErrWriteAfterFlush = errors.New("unused") // Deprecated
)
```
### Why
- Errors are package-level `var` (not `const`) — they're pointer values
- Error strings start with the package name (`"http: ..."`) for disambiguation in logs
- Deprecated errors are kept for backward compatibility but marked clearly
- Internal errors can be aliased (`ErrBodyNotAllowed = internal.ErrBodyNotAllowed`) to share across internal packages
### Convention: Error String Format
```
package: description
```
Examples from stdlib:
- `"http: connection has been hijacked"`
- `"sql: unknown driver %q (forgotten import?)"`
- `"json: unsupported type: %s"`
### Anti-pattern
```go
// DON'T: Capitalize error strings
errors.New("Connection has been hijacked") // Go convention: lowercase
// DON'T: End error strings with punctuation
errors.New("connection failed.") // no trailing period
```
---
## 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` |
+390
View File
@@ -0,0 +1,390 @@
# Go Interface Design Patterns
Patterns extracted from the Go standard library source code.
---
## 1. Small Interfaces (1-2 Methods)
### Source: `src/io/io.go:80-92` (Reader), `93-103` (Writer), `105-109` (Closer)
Go's most powerful interfaces have exactly **one method**:
```go
// src/io/io.go:80-92
type Reader interface {
Read(p []byte) (n int, err error)
}
// src/io/io.go:93-103
type Writer interface {
Write(p []byte) (n int, err error)
}
// src/io/io.go:105-109
type Closer interface {
Close() error
}
```
### Why
Small interfaces are easy to implement and easy to compose. Any type can satisfy `io.Reader` by implementing a single method. This maximizes the number of types that can participate in the ecosystem — files, network connections, buffers, compressors, encryptors all satisfy `Reader`.
### Anti-pattern
```go
// DON'T: Giant interface that tries to cover everything
type FileSystem interface {
Open(name string) (File, error)
Create(name string) (File, error)
Remove(name string) error
Stat(name string) (FileInfo, error)
ReadDir(name string) ([]DirEntry, error)
MkdirAll(path string, perm FileMode) error
// ... 10 more methods
}
```
Large interfaces are hard to implement, hard to mock, and couple consumers to capabilities they don't need.
---
## 2. Interface Composition
### Source: `src/io/io.go:131-155`
Compose small interfaces into larger ones only when needed:
```go
// src/io/io.go:131-134
type ReadWriter interface {
Reader
Writer
}
// src/io/io.go:136-139
type ReadCloser interface {
Reader
Closer
}
// src/io/io.go:141-144
type WriteCloser interface {
Writer
Closer
}
// src/io/io.go:146-150
type ReadWriteCloser interface {
Reader
Writer
Closer
}
```
### Why
Composition lets you express precisely what you need. A function that only reads should accept `Reader`, not `ReadWriteCloser`. Callers provide the minimum; composers build up.
### Anti-pattern
```go
// DON'T: Define a big interface, then use only part of it
func processData(rw ReadWriteCloser) {
// only calls Read... why require Write and Close?
}
```
---
## 3. Accept Interfaces, Return Structs
### Source: `src/io/io.go:461` (LimitReader), `src/io/io.go:618` (TeeReader)
```go
// src/io/io.go:461
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
// src/io/io.go:467-471
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
}
```
```go
// src/io/io.go:618-620
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}
```
### Why
- **Accept interfaces**: Maximizes what callers can pass in — any `Reader` works.
- **Return structs** (or concrete types): Gives callers access to the full type, including fields and methods not in any interface. `LimitedReader` exposes `R` and `N` publicly so callers can inspect remaining bytes.
The return type of `LimitReader` is `Reader` (interface), but the underlying value is `*LimitedReader` (struct). Functions like `io.Copy` can type-assert to `*LimitedReader` to optimize buffer sizes (line 425).
### Anti-pattern
```go
// DON'T: Accept concrete types
func LimitReader(r *os.File, n int64) Reader // too restrictive
// DON'T: Return interfaces when you have useful struct fields
func NewServer() ServerInterface // hides useful config fields
```
---
## 4. Interface Satisfaction as a Compile-Time Check
### Source: `src/io/io.go:645`, `src/net/http/server.go:4071`
```go
// src/io/io.go:645
var _ ReaderFrom = discard{}
// src/net/http/server.go:4071
var _ Pusher = (*timeoutWriter)(nil)
```
### Why
These blank-identifier declarations cause a compile error if the type stops implementing the interface. They're documentation and safety net combined — no runtime cost.
### Anti-pattern
```go
// DON'T: Discover interface failures at runtime
func doSomething(w ResponseWriter) {
pusher := w.(Pusher) // panics if w doesn't implement Pusher
}
```
---
## 5. Interface-Based Polymorphism (sort.Interface)
### Source: `src/sort/sort.go:16-41`
```go
// src/sort/sort.go:16-41
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
```
### Why
The sort package defines *behavior* it needs, not data it operates on. Any collection — slices, linked lists, database result sets — can be sorted by implementing three methods. The algorithm is decoupled from the data structure.
### Anti-pattern
```go
// DON'T: Hardcode to a specific data type
func Sort(data []int) // only works for []int
func Sort(data []interface{}) // loses type safety
```
Note: Since Go 1.21, `slices.SortFunc` is preferred for slices (generic + faster), but `sort.Interface` remains the pattern for non-slice collections.
---
## 6. The Adapter Pattern (HandlerFunc)
### Source: `src/net/http/server.go:2334-2342`
```go
// src/net/http/server.go:2334-2338
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// src/net/http/server.go:2341-2342
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
```
### Why
This bridges functions and interfaces. Any function with the right signature becomes a `Handler` via `HandlerFunc(myFunc)`. You get the simplicity of functions with the composability of interfaces.
### Anti-pattern
```go
// DON'T: Force users to create a struct just to implement an interface
type myHandler struct{}
func (h myHandler) ServeHTTP(w ResponseWriter, r *Request) {
// just calls a function anyway
handleHome(w, r)
}
```
---
## 7. Optional Interfaces (Runtime Feature Detection)
### Source: `src/net/http/server.go:165-175` (Flusher), `src/net/http/server.go:183-206` (Hijacker)
```go
// src/net/http/server.go:165-170
type Flusher interface {
Flush()
}
// src/net/http/server.go:183-206
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
```
Usage pattern (from doc comments):
```go
// Handlers should always test for this ability at runtime.
if flusher, ok := w.(Flusher); ok {
flusher.Flush()
}
```
### Why
Not every `ResponseWriter` supports flushing or hijacking (HTTP/2 doesn't support Hijacker). Instead of bloating the main interface, optional capabilities are separate interfaces checked at runtime. This keeps the core interface small while allowing progressive enhancement.
### Anti-pattern
```go
// DON'T: Put optional capabilities in the main interface
type ResponseWriter interface {
Write([]byte) (int, error)
WriteHeader(int)
Header() Header
Flush() // not always supported!
Hijack() (net.Conn, *bufio.ReadWriter, error) // not always supported!
}
```
---
## 8. The Stringer Interface (Convention-Based Behavior)
### Source: `src/fmt/print.go:63-66`
```go
// src/fmt/print.go:63-66
type Stringer interface {
String() string
}
```
### Why
`fmt` checks if a value implements `Stringer` and calls `String()` for its text representation. This is a *convention*: any type in any package can participate without importing `fmt`. The interface acts as a protocol.
Similarly, `GoStringer` (line 71) controls `%#v` output, and `Formatter` (line 58-61) gives total control over formatting.
### Anti-pattern
```go
// DON'T: Use type switches over known types
func printThing(v any) string {
switch v := v.(type) {
case MyType: return v.name
case OtherType: return v.label
// ... must know about every type
}
}
```
---
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
### Source: `src/io/io.go:410-417`
```go
// src/io/io.go:410-417
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
// If the reader has a WriteTo method, use it to do the copy.
// Avoids an allocation and a copy.
if wt, ok := src.(WriterTo); ok {
return wt.WriteTo(dst)
}
// Similarly, if the writer has a ReadFrom method, use it to do the copy.
if rf, ok := dst.(ReaderFrom); ok {
return rf.ReadFrom(src)
}
// ... fallback to buffer-based copy
}
```
### Why
The core function works with the minimum interface (`Reader`/`Writer`) but *upgrades* behavior when richer interfaces are available. `*os.File` implements `ReadFrom` using `sendfile(2)` — zero-copy kernel transfer. The generic path works, but specialized implementations optimize transparently.
### Anti-pattern
```go
// DON'T: Always use the slow generic path
func Copy(dst Writer, src Reader) {
buf := make([]byte, 32*1024)
// always allocates, never leverages kernel optimizations
}
```
---
## 10. The driver.Driver Pattern (Plugin Interfaces)
### Source: `src/database/sql/driver/driver.go:85-97`, `104-112`
```go
// src/database/sql/driver/driver.go:85-97
type Driver interface {
Open(name string) (Conn, error)
}
// src/database/sql/driver/driver.go:104-112
type DriverContext interface {
OpenConnector(name string) (Connector, error)
}
```
### Why
The `database/sql` package defines interfaces that driver authors implement. The base interface (`Driver`) is minimal (one method). Extended capabilities (`DriverContext`, `Pinger`, `Execer`, `Queryer`) are opt-in — the `sql` package checks for them at runtime. This allows the ecosystem to grow without breaking existing drivers.
### Anti-pattern
```go
// DON'T: One massive interface all drivers must implement
type Driver interface {
Open(name string) (Conn, error)
OpenConnector(name string) (Connector, error)
Ping(ctx context.Context) error
// ... every driver must implement everything, even if not supported
}
```
---
## Summary: Go Interface Design Principles
| Principle | Standard Library Example |
|-----------|------------------------|
| Keep interfaces small (1-2 methods) | `io.Reader`, `io.Writer`, `fmt.Stringer` |
| Compose small interfaces | `io.ReadWriteCloser` |
| Accept interfaces, return structs | `io.LimitReader`, `io.TeeReader` |
| Use adapter types for functions | `http.HandlerFunc` |
| Optional capabilities via separate interfaces | `http.Flusher`, `http.Hijacker` |
| Compile-time interface checks | `var _ Interface = (*Type)(nil)` |
| Runtime interface upgrade for optimization | `io.Copy``WriterTo`/`ReaderFrom` |
| Plugin/driver interfaces start minimal | `database/sql/driver.Driver` |
+551
View File
@@ -0,0 +1,551 @@
# 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."`
- Subsequent paragraphs: contracts, caveats, links to deeper docs
- For multi-file packages, put the package comment in `doc.go` or the primary file
### Anti-pattern
```go
// DON'T: No package comment
package myutil
// DON'T: Restate the obvious
// Package http provides HTTP stuff.
package http
// DON'T: Put implementation details in the package comment
// Package auth uses bcrypt with cost 12 and stores hashes in PostgreSQL.
package auth
```
---
## 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`
- `errors` — not `errs` or `errorhandling`
### Why
Go package names are:
- **Short** — one word, lowercase, no underscores or mixedCaps
- **Clear** — the name is the context for everything inside it
- **Singular** (usually) — `context` not `contexts`, `error` exception (`errors` has functions)
The package name is part of every qualified identifier: `http.Handler`, `json.Marshal`, `context.Context`. Redundancy in naming is wasted keystrokes:
```go
// Good: package name provides context
http.Server // not http.HTTPServer
json.Encoder // not json.JSONEncoder
context.Context // the type IS the context
```
### Anti-pattern
```go
// DON'T: Stutter (repeat package name in exported identifiers)
package http
type HTTPServer struct{} // http.HTTPServer — redundant
func NewHTTPClient() // http.NewHTTPClient — say "http" twice
// DON'T: Use utility/helper package names
package utils // what does it DO?
package helpers // grab bag, no cohesion
package common // everything ends up here
// DON'T: Use plural when singular works
package requests // should be: package request
```
---
## 3. internal/ Packages — Restricting Visibility
### Source: `src/net/http/internal/`, `src/encoding/json/internal.go`
```
src/net/http/internal/
├── ascii/
├── chunked.go
├── common.go
├── http2/
├── httpcommon/
├── httpsfv/
├── sniff.go
└── testcert/
```
### Why
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. For example:
- `net/http/internal/ascii` can be imported by `net/http` and `net/http/...`
- It **cannot** be imported by `net/url` or any other package
This lets you share code between sub-packages without making it part of the public API.
### Usage Guidelines
```
myproject/
├── internal/ # shared across the project, but not importable externally
│ ├── auth/
│ └── metrics/
├── cmd/
│ └── server/
└── pkg/ # actually public API (if you use this convention)
└── client/
```
### Anti-pattern
```go
// DON'T: Export implementation details that should be internal
package mylib
func HelperThatOnlyIUse() {} // pollutes API surface
// DON'T: Put everything in internal/ (nothing is reusable)
// Balance: internal/ for implementation; exported packages for contracts
```
---
## 4. Export Rules — The Capital Letter Boundary
### Source: Throughout stdlib — the convention is the language itself
```go
// src/io/io.go
var EOF = errors.New("EOF") // exported: uppercase
var errInvalidWrite = errors.New(...) // unexported: lowercase
// src/io/io.go:622-625
type teeReader struct { // unexported type
r Reader
w Writer
}
// src/io/io.go:618
func TeeReader(r Reader, w Writer) Reader { // exported constructor
return &teeReader{r, w}
}
```
### Why
The exported/unexported boundary is Go's encapsulation mechanism. `teeReader` is unexported because:
1. Users don't need to know its implementation
2. The return type is `Reader` (the interface) — maximum flexibility
3. The struct's fields can change without breaking anyone
### Pattern: Exported Function, Unexported Type
```go
// Export the constructor, not the type
func NewParser(r io.Reader) *parser { ... } // WRONG: can't return unexported type
// Correct: return via interface or exported type
func TeeReader(r Reader, w Writer) Reader { return &teeReader{r, w} }
```
### Anti-pattern
```go
// DON'T: Export everything "just in case"
type Parser struct {
Input string // should this be settable? probably not
buffer []byte // internal state — definitely not
pos int
}
// DON'T: Make internal state accessible
type DB struct {
Pool []*Conn // callers shouldn't manipulate the pool directly
}
```
---
## 5. init() Functions — Use Sparingly
### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31`
```go
// src/net/http/http2.go:37
func init() {
// register HTTP/2 protocol implementation
}
```
### Why
`init()` runs automatically at program start, in dependency order. The stdlib uses it for:
- **Driver registration** (database drivers register via init)
- **Protocol negotiation** (HTTP/2 registers its handler)
- **Configuration from build tags** (`servemux121.go` — compatibility shim)
### Rules
1. `init()` should have no side effects beyond registration
2. No errors should be possible (can't return error from init)
3. Keep them short — they block program startup
4. Prefer explicit initialization in `main()` when possible
### Anti-pattern
```go
// DON'T: Do heavy work in init
func init() {
db = connectToDatabase() // fails silently, crashes later
cache = loadGigabyteFile() // blocks startup
}
// DON'T: Use init for configuration
func init() {
port = os.Getenv("PORT") // harder to test, implicit dependency
}
// DO: Prefer explicit setup
func main() {
db, err := connectToDatabase()
if err != nil {
log.Fatal(err) // clear failure point
}
}
```
---
## 6. Functional Options Pattern
### Source: Not directly in stdlib, but `net/http.Server` and `database/sql.DB` demonstrate the problem it solves
The stdlib uses struct-based configuration (Server, Transport, DB config via setters). The functional options pattern emerged from the community to solve the "many optional parameters" problem:
```go
// The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney):
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
```
### What the stdlib uses instead: Config structs
```go
// src/net/http/server.go (Server struct acts as config)
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 are data (stdlib preference) |
| Functional options | Many options, some involve behavior, public API stability matters |
| Builder pattern | Rare in Go — usually overkill |
### Anti-pattern
```go
// DON'T: Long parameter lists
func NewServer(addr string, timeout time.Duration, maxConns int,
logger *log.Logger, tls *tls.Config, handler Handler) *Server
// DON'T: Use functional options when a simple struct suffices
// (Over-engineering for 2-3 fields)
```
---
## 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),
lastPut: make(map[*driverConn]string),
stop: cancel,
}
go db.connectionOpener(ctx)
return db
}
```
### Why
- `NewX()` when construction is trivial (just allocate)
- `OpenX()` or `NewXWithConfig()` when construction involves resources, validation, or can fail
- Return `*T` (pointer to concrete type), not an interface
The zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`), making constructors unnecessary.
### Anti-pattern
```go
// DON'T: Constructor that returns interface (hides useful methods)
func NewWriter() io.Writer { return &myWriter{} }
// DON'T: Require constructor when zero value works
type Buffer struct {
buf []byte
// ...
}
// var b bytes.Buffer ← just works, no New needed
```
---
## 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/ # encoding interfaces
│ ├── json/ # JSON codec
│ └── xml/ # XML codec
├── database/
│ └── sql/ # SQL database abstraction
│ └── driver/ # SPI for database drivers
└── context/ # cancellation propagation
```
### Why
Each package has a single, clear responsibility:
- `io` defines interfaces; `os` implements them for files
- `encoding/json` handles JSON; `encoding/xml` handles XML
- `database/sql` is the user-facing API; `database/sql/driver` is the implementor-facing SPI
### Anti-pattern
```go
// DON'T: Package per type
package user // just has User struct
package order // just has Order struct
package payment // just has Payment struct
// 50 packages with 1 file each — Go prefers fewer, larger packages
// DON'T: Circular dependencies
package a imports package b
package b imports package a // compile error
// FIX: Extract shared types into a third package, or merge
```
---
## 9. API Design — database/sql Separation of Concerns
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
Two distinct APIs in one subsystem:
**User-facing (database/sql):**
```go
db, _ := sql.Open("postgres", connStr)
rows, _ := db.QueryContext(ctx, "SELECT ...")
defer rows.Close()
for rows.Next() {
rows.Scan(&id, &name)
}
```
**Driver-facing (database/sql/driver):**
```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 a high-level, safe API with pooling and retry
- Drivers implement a low-level, minimal interface
- The `sql` package mediates between them
### Anti-pattern
```go
// DON'T: Expose implementation to users
type DB struct {
driver driver.Conn // users shouldn't touch this
}
// DON'T: Mix user and implementor APIs in one interface
type Database interface {
Query(sql string) Rows // user method
Open(dsn string) Conn // driver method — different audiences
}
```
---
## 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 comment)
// Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// type key int
//
// var userKey key
//
// func NewContext(ctx context.Context, u *User) context.Context {
// 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 or overwriting your values
- **Type-safe accessors** (`FromContext`) avoid type assertions at every call site
- **Pointer-based keys** (`&contextKey{...}`) guarantee uniqueness even with same string names
### Anti-pattern
```go
// DON'T: Use string keys (any package can collide)
ctx = context.WithValue(ctx, "user", user)
// DON'T: Use exported key types (anyone can access)
type Key string
const UserKey Key = "user" // other packages can use this key
// DON'T: Store optional parameters in context
ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
```
---
## Summary: Package Design Principles
| Principle | Rule |
|-----------|------|
| Package comment | `"Package X does Y."` before `package` keyword |
| Naming | Short, lowercase, no stutter (`http.Server` not `http.HTTPServer`) |
| Encapsulation | `internal/` for shared-but-private code |
| Exports | Minimum viable surface; unexported by default |
| init() | Only for registration; keep trivial |
| Constructors | `NewX()``*T`; prefer usable zero values |
| Organization | One concern per package; no circular deps |
| API layers | Separate user-facing from implementor-facing (SPI) |
| Context values | Unexported key type + typed accessors |
| Configuration | Struct literals (stdlib) or functional options (community) |
+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)
```
+670
View File
@@ -0,0 +1,670 @@
# Go Testing Patterns
Patterns extracted from the Go standard library source code.
---
## 1. Table-Driven Tests with Subtests
### Source: `src/encoding/json/encode_test.go:405-430`
```go
// src/encoding/json/encode_test.go:405-430
func TestUnsupportedValues(t *testing.T) {
tests := []struct {
CaseName
in any
}{
{Name(""), math.NaN()},
{Name(""), math.Inf(-1)},
{Name(""), math.Inf(1)},
{Name(""), pointerCycle},
{Name(""), pointerCycleIndirect},
{Name(""), mapCycle},
{Name(""), sliceCycle},
{Name(""), recursiveSliceCycle},
}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
if _, err := Marshal(tt.in); err != nil {
if _, ok := err.(*UnsupportedValueError); !ok {
t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError))
}
} else {
t.Errorf("%s: Marshal error: got nil, want non-nil", tt.Where)
}
})
}
}
```
### Source: `src/encoding/json/encode_test.go:270-328` (with inputs and expected outputs)
```go
// src/encoding/json/encode_test.go:270-328
func TestRoundtripStringTag(t *testing.T) {
tests := []struct {
CaseName
in StringTag
want string
}{{
CaseName: Name("AllTypes"),
in: StringTag{
BoolStr: true,
IntStr: 42,
UintptrStr: 44,
StrStr: "xzbit",
NumberStr: "46",
},
want: `{
"BoolStr": "true",
"IntStr": "42",
...
}`,
}, {
CaseName: Name("StringDoubleEscapes"),
in: StringTag{
StrStr: "\b\f\n\r\t\"\\",
NumberStr: "0",
},
want: `{...}`,
}}
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
got, err := MarshalIndent(&tt.in, "", "\t")
if err != nil {
t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err)
}
if got := string(got); got != tt.want {
t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, ...)
}
// Verify round-trip
var s2 StringTag
if err := Unmarshal(got, &s2); err != nil {
t.Fatalf("%s: Decode error: %v", tt.Where, err)
}
if !reflect.DeepEqual(s2, tt.in) {
t.Fatalf("%s: Decode:\n\tinput: %s\n\tgot: %#v\n\twant: %#v", ...)
}
})
}
}
```
### Why
Table-driven tests are Go's signature testing pattern:
1. **All cases visible in one place** — easy to add new cases
2. **t.Run creates subtests** — each case runs independently, can be filtered with `-run`
3. **Uniform structure** — input, expected output, test name
4. **Failures identify which case** — via the case name
### Template
```go
func TestFoo(t *testing.T) {
tests := []struct {
name string
input InputType
want OutputType
wantErr bool
}{
{name: "basic", input: ..., want: ...},
{name: "empty", input: ..., want: ...},
{name: "error case", input: ..., wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Foo(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("Foo() error = %v, wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("Foo() = %v, want %v", got, tt.want)
}
})
}
}
```
### Anti-pattern
```go
// DON'T: Separate test functions for each case
func TestFoo_Basic(t *testing.T) { ... }
func TestFoo_Empty(t *testing.T) { ... }
func TestFoo_Error(t *testing.T) { ... }
// 50 near-identical functions — hard to maintain
// DON'T: Tests without names (hard to identify failures)
tests := []struct{ in, want int }{{1, 2}, {3, 4}}
for _, tt := range tests {
// which test failed? index 0 or 1?
}
```
---
## 2. t.Helper() — Clean Error Reporting
### Source: `src/testing/testing.go:1415-1435`
```go
// src/testing/testing.go:1415-1435
func (c *common) Helper() {
if c.isSynctest {
c = c.parent
}
c.mu.Lock()
defer c.mu.Unlock()
if c.helperPCs == nil {
c.helperPCs = make(map[uintptr]struct{})
}
var pc [1]uintptr
n := runtime.Callers(2, pc[:])
if n == 0 {
panic("testing: zero callers found")
}
if _, found := c.helperPCs[pc[0]]; !found {
c.helperPCs[pc[0]] = struct{}{}
c.helperNames = nil
}
}
```
### Why
When a test helper calls `t.Helper()`, failures report the **caller's** line number, not the helper's. Without it, every failure points to the helper function — useless for identifying which test case failed.
### Idiomatic Usage
```go
func assertNoError(t *testing.T, err error) {
t.Helper() // failures point to the caller, not this line
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
func assertEqual(t *testing.T, got, want any) {
t.Helper()
if !reflect.DeepEqual(got, want) {
t.Errorf("got %v, want %v", got, want)
}
}
```
### Anti-pattern
```go
// DON'T: Forget t.Helper() in test helpers
func checkResult(t *testing.T, got, want string) {
// Missing t.Helper()
if got != want {
t.Errorf("got %q, want %q", got, want)
// Error points HERE, not the actual test case — confusing
}
}
```
---
## 3. t.Run() — Subtests and Test Organization
### Source: `src/testing/testing.go:2204-2260`
```go
// src/testing/testing.go:2204-2215
func (t *T) Run(name string, f func(t *T)) bool {
t.hasSub.Store(true)
testName, ok, _ := t.tstate.match.fullName(&t.common, name)
if !ok || shouldFailFast() {
return true
}
// ...
ctx, cancelCtx := context.WithCancel(context.Background())
t = &T{
common: common{
name: testName,
parent: &t.common,
// ...
},
}
go tRunner(t, f)
// ...
}
```
### Why
Subtests:
1. **Run in separate goroutines** — isolated from each other
2. **Have their own context**`context.WithCancel(context.Background())`
3. **Can be filtered**`go test -run TestFoo/subcase`
4. **Can run in parallel** — via `t.Parallel()` inside the subtest
5. **Share setup/teardown** — parent test's defer runs after all subtests
### Pattern: Setup/Teardown with Subtests
```go
func TestDB(t *testing.T) {
db := setupTestDB(t) // shared setup
t.Cleanup(func() { db.Close() }) // runs after ALL subtests
t.Run("Insert", func(t *testing.T) {
// uses db
})
t.Run("Query", func(t *testing.T) {
// uses db
})
}
```
### Anti-pattern
```go
// DON'T: Rely on test execution order
func TestInsert(t *testing.T) { ... } // must run before TestQuery
func TestQuery(t *testing.T) { ... } // depends on TestInsert's side effects
// Tests should be independent!
```
---
## 4. t.Parallel() — Concurrent Test Execution
### Source: `src/testing/testing.go:1912`
```go
// src/testing/testing.go:1912
func (t *T) Parallel() {
// ...marks test as parallel, pauses until parent completes
}
```
### Why
`t.Parallel()` signals that this test can run concurrently with other parallel tests. The test pauses until its parent test function returns, then runs alongside other parallel subtests.
### Idiomatic Usage
```go
func TestFoo(t *testing.T) {
tests := []struct{
name string
input int
}{
{"small", 1},
{"large", 1000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // runs concurrently with other subtests
result := expensiveOperation(tt.input)
if result != expected {
t.Errorf(...)
}
})
}
}
```
### Anti-pattern
```go
// DON'T: Parallel tests that share mutable state
var counter int
func TestA(t *testing.T) {
t.Parallel()
counter++ // DATA RACE
}
func TestB(t *testing.T) {
t.Parallel()
counter++ // DATA RACE
}
// DON'T: Capture loop variable in Go < 1.22 without explicit copy
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// In Go < 1.22: tt is shared — always sees last value
// In Go >= 1.22: loop variables are per-iteration (fixed)
})
}
```
---
## 5. t.Cleanup() — Deterministic Teardown
### Source: `src/testing/testing.go:1439`
```go
// src/testing/testing.go:1439-1442
// Cleanup registers a function to be called when the test (or subtest)
// and all its subtests complete. Cleanup functions will be called in
// last added, first called order.
func (c *common) Cleanup(f func()) { ... }
```
### Why
`t.Cleanup` is like `defer` but tied to test lifecycle, not function scope. It runs after all subtests complete and works with parallel tests.
### Idiomatic Usage
```go
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
// Caller doesn't need to worry about cleanup:
func TestQueries(t *testing.T) {
db := setupTestDB(t) // automatically cleaned up
// ...
}
```
### Anti-pattern
```go
// DON'T: Return cleanup functions (easy to forget)
func setupDB(t *testing.T) (*sql.DB, func()) {
db := ...
return db, func() { db.Close() }
}
// Caller must remember: db, cleanup := setupDB(t); defer cleanup()
// DO: Use t.Cleanup inside the setup function
```
---
## 6. t.TempDir() — Automatic Temp Directories
### Source: `src/testing/testing.go:1575`
```go
// src/testing/testing.go:1575
func (c *common) TempDir() string { ... }
```
### Why
Creates a temp directory that's automatically removed when the test completes. No manual cleanup needed, no leftover test artifacts.
### Idiomatic Usage
```go
func TestWriteConfig(t *testing.T) {
dir := t.TempDir() // auto-cleaned
path := filepath.Join(dir, "config.json")
err := WriteConfig(path, myConfig)
if err != nil {
t.Fatal(err)
}
got, _ := os.ReadFile(path)
// assert contents...
}
```
### Anti-pattern
```go
// DON'T: Create temp dirs manually and forget cleanup
func TestWrite(t *testing.T) {
dir, _ := os.MkdirTemp("", "test")
// forgot os.RemoveAll(dir) — test leaves garbage
}
```
---
## 7. testdata/ Directory
### Source: `src/net/http/testdata/`, `src/encoding/json/testdata/` (implicit — exists in the tree)
```
src/net/http/testdata/
├── file
├── index.html
└── style.css
```
### Why
The `testdata/` directory is special in Go:
1. **Ignored by the go tool** — not compiled as a package
2. **Available to tests** — accessed via relative path `"testdata/file.txt"`
3. **Committed to repo** — test fixtures live alongside the code
4. **Portable** — tests work without external dependencies
### Idiomatic Usage
```go
func TestParse(t *testing.T) {
input, err := os.ReadFile("testdata/input.json")
if err != nil {
t.Fatal(err)
}
want, err := os.ReadFile("testdata/expected.json")
if err != nil {
t.Fatal(err)
}
got := Parse(input)
if !bytes.Equal(got, want) {
t.Errorf("Parse mismatch")
}
}
```
### Golden File Pattern
```go
func TestOutput(t *testing.T) {
got := generateOutput()
golden := filepath.Join("testdata", t.Name()+".golden")
if *update { // -update flag to regenerate
os.WriteFile(golden, got, 0644)
}
want, _ := os.ReadFile(golden)
if !bytes.Equal(got, want) {
t.Errorf("output mismatch; run with -update to regenerate")
}
}
```
### Anti-pattern
```go
// DON'T: Embed large test fixtures as string literals
var testInput = `{
"very": "long",
"json": "string",
// 500 lines...
}`
// DON'T: Depend on external URLs for test data
func TestParse(t *testing.T) {
resp, _ := http.Get("https://example.com/test.json") // flaky!
}
```
---
## 8. Error Message Formatting
### Source: `src/encoding/json/encode_test.go` (throughout)
```go
// Pattern from encode_test.go:304
t.Fatalf("%s: MarshalIndent error: %v", tt.Where, err)
// Pattern from encode_test.go:306-307
t.Fatalf("%s: MarshalIndent:\n\tgot: %s\n\twant: %s", tt.Where, got, want)
// Pattern from encode_test.go:421-422
t.Errorf("%s: Marshal error:\n\tgot: %T\n\twant: %T", tt.Where, err, new(UnsupportedValueError))
```
### Why
The stdlib follows a consistent error format:
- **Context first** — where/what was being tested
- **got/want on separate lines** — easy to diff visually
- **Tab-indented** — aligns the comparison
### Convention
```
t.Errorf("FunctionName(%v) = %v, want %v", input, got, want)
// or for complex values:
t.Errorf("FunctionName(%v):\n\tgot: %v\n\twant: %v", input, got, want)
```
### Anti-pattern
```go
// DON'T: Vague error messages
t.Error("failed") // what failed? what was expected?
// DON'T: Only show the got value
t.Errorf("got %v", got) // what was expected?
// DON'T: Use assert libraries that hide the actual comparison
assert.Equal(t, got, want) // when it fails: "not equal" — which is which?
```
---
## 9. t.Fatal vs t.Error
### Convention across stdlib
```go
// Fatal: test cannot continue meaningfully
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err) // no point continuing without a database
}
// Error: test can continue, report all failures
for _, tt := range tests {
got := fn(tt.input)
if got != tt.want {
t.Errorf(...) // report and keep going
}
}
```
### Why
- `t.Fatal` / `t.Fatalf`**stops the test immediately**. Use for setup failures where subsequent assertions are meaningless.
- `t.Error` / `t.Errorf`**reports failure, continues**. Use in loops to collect all failures at once.
### Anti-pattern
```go
// DON'T: Fatal in loops (misses subsequent failures)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got != tt.want {
t.Fatalf(...) // stops this subtest — fine in subtests actually
}
})
}
// But without subtests:
for _, tt := range tests {
if got != tt.want {
t.Fatalf(...) // stops ALL remaining cases!
}
}
// DON'T: Error when you can't continue
file, err := os.Open(path)
if err != nil {
t.Errorf("open: %v", err) // continues to use nil file — panic!
}
file.Read(...)
```
---
## 10. Test Naming Conventions
### Source: All stdlib tests follow these patterns
```go
// Function tests: TestFunctionName
func TestMarshal(t *testing.T) { ... }
// Method tests: TestTypeName_MethodName
func TestEncoder_Encode(t *testing.T) { ... }
// Behavior tests: TestDescription
func TestOmitEmpty(t *testing.T) { ... }
// Edge cases: TestFunctionName_EdgeCase
func TestMarshal_NilSlice(t *testing.T) { ... }
// Benchmarks: BenchmarkFunctionName
func BenchmarkMarshal(b *testing.B) { ... }
// Examples: ExampleFunctionName
func ExampleMarshal() {
// ...
// Output: {"name":"Alice"}
}
```
### Why
- Test names are used with `-run` flag for filtering
- They appear in failure output — should be self-explanatory
- Example functions become documentation (shown in godoc)
- Subtests use `/` separator: `TestMarshal/NilSlice`
### Anti-pattern
```go
// DON'T: Numbered tests
func Test1(t *testing.T) { ... }
func Test2(t *testing.T) { ... }
// DON'T: Tests that don't start with Test
func testHelper(t *testing.T) { ... } // won't be run! (lowercase 't')
// DON'T: Overly verbose names
func TestThatMarshalCorrectlyHandlesNilSliceInputAndReturnsNullJSON(t *testing.T) { ... }
```
---
## Summary: Testing Best Practices
| Pattern | When |
|---------|------|
| Table-driven tests | Multiple inputs for same logic |
| t.Run subtests | Isolate cases, enable `-run` filtering |
| t.Helper() | Every test helper function |
| t.Parallel() | Independent tests, speed up suite |
| t.Cleanup() | Resource teardown (replaces defer in helpers) |
| t.TempDir() | Need filesystem for test |
| testdata/ | External test fixtures |
| Golden files | Complex expected output |
| t.Fatal | Setup failures (can't continue) |
| t.Error | Assertion failures (collect all) |
| got/want format | `"got %v, want %v"` or `"\n\tgot: %v\n\twant: %v"` |