Compare commits

..
1 Commits
19 changed files with 1590 additions and 6748 deletions
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 Aaron Weiker
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-58
View File
@@ -1,58 +0,0 @@
# Go Patterns
**Prescriptive.** Follow these when writing Go code.
A pattern is a reusable solution to a recurring problem. Each one has:
- **When to use** — the problem it solves
- **When NOT to use** — where it causes harm
- **Why** — the reasoning, not just the rule
- **Source citations** — verified file:line from real codebases
These are derived from what mature Go codebases *actually do*, not opinions or blog posts.
## Structure
- `patterns/` — what to do (interfaces, errors, concurrency, testing, packages, etc.)
- `smells/` — what NOT to do (anti-patterns, common mistakes)
- `sources/` — reference material from specific projects (golang/go, Prometheus). Study for ideas, don't copy blindly.
## How to use
Give your agent these instructions depending on the task:
### Solving a problem
> You have access to a patterns repo containing proven solutions to recurring Go problems. When I describe a problem:
>
> 1. Identify which pattern files are relevant (read them)
> 2. Check if my problem matches a "When to use" case
> 3. Check if it matches a "When NOT to use" case
> 4. If a pattern fits: suggest the approach, cite the pattern, explain why it applies here
> 5. If no pattern fits: say so, and suggest an approach grounded in the principles you see across the patterns
> 6. If my problem matches a smell: warn me before I make the mistake
>
> Never suggest something that contradicts a documented pattern without explicitly calling out the deviation and justifying it.
### Reviewing code
> You have access to a patterns repo that defines how Go code should be written. For each file in the diff:
>
> 1. Read the relevant pattern files
> 2. Verify the code follows the documented patterns
> 3. If it deviates: flag it with a reference to the specific pattern, section, and why it matters
> 4. If it matches a smell: flag it as a known anti-pattern
> 5. A deviation without justification is a finding
>
> Don't invent rules. Only flag what the patterns document.
### Evaluating a pattern
> Read the pattern file. Compare against how the following projects handle the same problem: [list projects]. Does the pattern hold? Are there cases where it breaks down? Should it be updated, split, or retired? File your findings as an issue.
## Patterns vs Conventions
**Pattern** = prescriptive. "When you face X, do Y." Language-scoped. Follow these.
**Convention** = descriptive. "Project Z does it this way." Context-specific. Study for ideas — applying another project's conventions to yours without understanding their constraints causes harm.
The `sources/` directory is convention material absorbed from thin repos. The `patterns/` directory is what you actually follow.
-567
View File
@@ -1,567 +0,0 @@
# API Conventions in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. The Must Pattern
**Pattern name:** MustXxx (Panic on Error)
**Source citation:** [regexp/regexp.go#L310](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L310), [text/template/helper.go#L19](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/text/template/helper.go#L19)
**What it does:** A function wraps a fallible constructor and panics if the error
is non-nil. Named `MustXxx` or `Must` (when wrapping a generic `(T, error)` pair).
**Why:** Safe initialization of package-level variables at program startup. Since
`var` initializers can't handle errors, `Must` converts programmer errors (bad
regex literals, bad templates) into immediate panics that surface during init.
**When to Use**
**Triggers:**
- You're initializing a package-level variable with a value that is known at compile time (regex, template, URL)
- Failure means a programmer bug, not a runtime condition (the regex literal is wrong, not user input)
- `var` initialization can't handle the `(T, error)` return
**Example — before:**
```go
var emailRegex *regexp.Regexp
func init() {
var err error
emailRegex, err = regexp.Compile(`^[a-z]+@[a-z]+\.[a-z]+$`)
if err != nil {
panic(err) // manual panic, verbose
}
}
```
**Example — after:**
```go
var emailRegex = regexp.MustCompile(`^[a-z]+@[a-z]+\.[a-z]+$`)
// One line. Panics on typo (caught immediately in tests). Clean.
```
### When NOT to Use
**Don't use this when:**
- The input is dynamic or user-provided (URL from a config file, regex from user input)
- You're inside a request handler or any code path where panicking would crash the server
- The error is recoverable — the caller should decide how to handle it
**Over-application example:**
```go
func HandleSearch(w http.ResponseWriter, r *http.Request) {
pattern := r.URL.Query().Get("q")
re := regexp.MustCompile(pattern) // PANIC on invalid user input!
// One bad query crashes the entire server
matches := re.FindAllString(corpus, -1)
// ...
}
```
**Better alternative:**
```go
func HandleSearch(w http.ResponseWriter, r *http.Request) {
pattern := r.URL.Query().Get("q")
re, err := regexp.Compile(pattern)
if err != nil {
http.Error(w, "invalid regex: "+err.Error(), 400)
return
}
matches := re.FindAllString(corpus, -1)
// ...
}
```
**Why:** `Must` is for programmer errors caught at init time, not for runtime input.
If the input can vary, the error is expected and must be handled — not panicked on.
**Anti-pattern:** Using Must in runtime code where the input is dynamic/user-provided;
panicking on recoverable errors; naming it something other than Must (e.g., `PanicOnError`).
**Code examples from source:**
```go
// regexp/regexp.go:310-320
// MustCompile is like [Compile] but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
```
```go
// text/template/helper.go:19-30
// Must is a helper that wraps a call to a function returning ([*Template], error)
// and panics if the error is non-nil. It is intended for use in variable
// initializations such as
//
// var t = template.Must(template.New("name").Parse("text"))
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}
```
---
## 2. Compile / MustCompile Pair
**Pattern name:** Fallible Constructor + Must Wrapper
**Source citation:** [regexp/regexp.go#L130](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L130), 310–320
**What it does:** The real constructor returns `(*T, error)`. A parallel `Must` variant
wraps it for use in global variable initialization.
**Why:** Separates concerns: `Compile` is for runtime use where errors are handled;
`MustCompile` is for compile-time-known values where failure is a programming bug.
**Anti-pattern:** Only providing the Must variant (no way to handle errors gracefully);
only providing the error variant (verbose for package-level vars).
**Code example from source:**
```go
// regexp/regexp.go:130-131
func Compile(expr string) (*Regexp, error) {
return compile(expr, syntax.Perl, false)
}
// regexp/regexp.go:310-315
func MustCompile(str string) *Regexp {
regexp, err := Compile(str)
if err != nil {
panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
}
return regexp
}
```
---
## 3. XxxWithContext Variant
**Pattern name:** WithContext Function Overload
**Source citation:** [net/http/request.go#L867](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/request.go#L867), 894–930
**What it does:** Provides two function variants: `NewRequest` (uses `context.Background()`)
and `NewRequestWithContext` (accepts an explicit context). The simple version delegates
to the context-aware one.
**Why:** Context was added after the original API was established. The `WithContext`
variant enables cancellation and deadlines; the plain variant preserves backward
compatibility and ergonomics for the common case.
**Anti-pattern:** Breaking the existing API signature; always requiring context even
for fire-and-forget uses; naming it `NewRequestCtx`.
**Code example from source:**
```go
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
// net/http/request.go:894+
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
// full implementation...
}
```
---
## 4. Nil-Opts Convention (Optional Config Pointer)
**Pattern name:** `*Options` Parameter — Nil Means Defaults
**Source citation:** [log/slog/text_handler.go#L28](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/text_handler.go#L28), [log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135)
**What it does:** A constructor accepts a pointer to an options struct. If the pointer
is nil, all defaults apply. The constructor internally substitutes a zero-value struct.
**Why:** Keeps the simple case clean (`NewTextHandler(os.Stderr, nil)`) while allowing
full customization. The pointer type signals "this entire argument is optional."
**Anti-pattern:** Requiring a non-nil options struct even with zero customization;
using variadic functional options when a simple struct suffices.
**Code example from source:**
```go
// log/slog/text_handler.go:28-42
// NewTextHandler creates a [TextHandler] that writes to w,
// using the given options.
// If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
return &TextHandler{
&commonHandler{
json: false,
w: w,
opts: *opts,
mu: &sync.Mutex{},
},
}
}
```
---
## 5. Builder Pattern (Accumulate + Finalize)
**Pattern name:** Builder (Write Methods + String/Bytes Finalizer)
**Source citation:** [strings/builder.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L14)
**What it does:** A zero-value struct accumulates data via Write/WriteByte/WriteString
methods, then produces a final result via String(). The builder is not reusable after
a copyCheck-protected modification.
**Why:** Avoids repeated string concatenation (O(n²) allocations). The zero value is
ready to use. Implements `io.Writer` so it integrates with `fmt.Fprintf`, etc.
**Anti-pattern:** Allocating on every append; requiring explicit initialization;
not implementing standard interfaces (`io.Writer`).
**Code example from source:**
```go
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
// strings/builder.go:112-116
func (b *Builder) WriteString(s string) (int, error) {
b.copyCheck()
b.buf = append(b.buf, s...)
return len(s), nil
}
// strings/builder.go:46-48
func (b *Builder) String() string {
return unsafe.String(unsafe.SliceData(b.buf), len(b.buf))
}
```
---
## 6. Layered API (Convenience → Full Control)
**Pattern name:** Convenience Wrappers over Configurable Core
**Source citation:** [os/file.go#L385](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L385)
**What it does:** Simple functions (`Open`, `Create`) delegate to the fully configurable
`OpenFile` with pre-set flags. Users choose their level of control.
**Why:** 90% of file opens are reads or creates. Layered APIs serve the common case
without hiding power. The naming makes intent clear.
**When to Use**
**Triggers:**
- 90% of callers need the simple case (open for read, create and truncate)
- You have a powerful function with many flags/options but most combinations are rare
- You find yourself writing the same flag combination repeatedly in calling code
**Example — before:**
```go
// User must know about flags for every file open
f, err := os.OpenFile("data.json", os.O_RDONLY, 0)
// Every. Single. Time.
```
**Example — after:**
```go
// Simple case:
f, err := os.Open("data.json") // just reads — no flags to remember
// Power case (when you actually need it):
f, err := os.OpenFile("data.json", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
```
### When NOT to Use
**Don't use this when:**
- There's no clear "common case" — all callers need different flag combinations
- The convenience wrapper would hide important behavior (e.g., `Create` hides truncation — some callers are surprised)
- You have 2+ equally common usage patterns that would each need their own wrapper, leading to an explosion of functions
**Over-application example:**
```go
// Too many convenience wrappers — which one do I want?
func OpenForAppend(name string) (*File, error) { ... }
func OpenOrCreate(name string) (*File, error) { ... }
func OpenReadWrite(name string) (*File, error) { ... }
func OpenExclusive(name string) (*File, error) { ... }
// Users now have to remember 6 functions instead of learning 1 + flags
```
**Better alternative:**
```go
// One convenience for the overwhelmingly common case, full-power for the rest
func Open(name string) (*File, error) { return OpenFile(name, O_RDONLY, 0) }
func Create(name string) (*File, error) { return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666) }
func OpenFile(name string, flag int, perm FileMode) (*File, error) { ... }
// Only 2 convenience wrappers for the 2 dominant patterns. Everything else uses OpenFile.
```
**Why:** Layered APIs work when there's a clear 80/20 split. If you're writing a convenience
wrapper for every combination, you've just created a larger API surface that's harder to
navigate than the single configurable function.
**Anti-pattern:** Only exposing the full-power version; making users learn flag
constants for simple reads; duplicating implementation across convenience functions.
**Code example from source:**
```go
// os/file.go:389-393
// Open opens the named file for reading.
func Open(name string) (*File, error) {
return OpenFile(name, O_RDONLY, 0)
}
// os/file.go:399-403
// Create creates or truncates the named file.
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
// os/file.go:410+ (the general form)
func OpenFile(name string, flag int, perm FileMode) (*File, error) {
// ...
}
```
---
## 7. Package-Level Functions Delegating to DefaultXxx
**Pattern name:** Convenience Package Functions
**Source citation:** [net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L109), implied by `http.Get`, `http.Post`
**What it does:** Top-level functions like `http.Get(url)` call methods on the
`DefaultClient`. Users can bypass by creating their own `Client`.
**Why:** Makes the simple case trivial (one-liner HTTP requests). No import of
constructors or setup needed. The package "just works" for basic usage.
**Anti-pattern:** Not providing convenience functions (forcing explicit construction
even for prototyping); making the default's behavior non-obvious.
**Code example from source:**
```go
// net/http/client.go:109
var DefaultClient = &Client{}
// net/http/client.go (implied pattern):
// func Get(url string) (resp *Response, err error) {
// return DefaultClient.Get(url)
// }
```
---
## 8. Register Pattern (Pluggable Algorithms)
**Pattern name:** RegisterXxx for Side-Effect Imports
**Source citation:** [crypto/crypto.go#L145](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L145)
**What it does:** A `RegisterHash(h Hash, f func() hash.Hash)` function allows
algorithm implementations in sub-packages to register themselves via `init()`.
The main package dispatches based on the registered factories.
**Why:** Decouples the algorithm registry from specific implementations. Users import
only the algorithms they need (e.g., `_ "crypto/sha256"`). Reduces binary size and
avoids circular dependencies.
**Anti-pattern:** Hard-coding all implementations; requiring explicit constructor calls
for each algorithm; using global mutable state without clear ownership.
**Code example from source:**
```go
// crypto/crypto.go:145-150
func RegisterHash(h Hash, f func() hash.Hash) {
if h == 0 || h >= maxHash {
panic("crypto: RegisterHash of unknown hash function")
}
hashes[h] = f
}
```
---
## 9. Graceful Shutdown Pattern
**Pattern name:** Close vs Shutdown (Immediate vs Graceful)
**Source citation:** [net/http/server.go#L3171](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3171) (Close), 3221+ (Shutdown)
**What it does:** Provides both `Close()` (immediate, forceful) and `Shutdown(ctx)`
(graceful, waits for in-flight requests). The context on Shutdown provides a
timeout mechanism.
**Why:** Different operational scenarios need different termination semantics.
Graceful shutdown is critical for production services; immediate close is needed for
tests and emergency stops.
**When to Use**
**Triggers:**
- Your type manages long-lived connections or in-flight requests
- You need both "stop now" (tests, emergencies) and "drain gracefully" (deploys, SIGTERM)
- A `Close()` that waits forever would make tests hang
**Example — before:**
```go
type Server struct { listener net.Listener }
func (s *Server) Stop() {
s.listener.Close() // all in-flight requests get connection reset — data loss
}
```
**Example — after:**
```go
type Server struct {
listener net.Listener
active sync.WaitGroup
}
// Immediate: drop everything
func (s *Server) Close() error {
return s.listener.Close()
}
// Graceful: stop accepting, wait for in-flight with timeout
func (s *Server) Shutdown(ctx context.Context) error {
s.listener.Close() // stop accepting new connections
done := make(chan struct{})
go func() { s.active.Wait(); close(done) }()
select {
case <-done:
return nil // all requests finished
case <-ctx.Done():
return ctx.Err() // timed out — caller decides what to do
}
}
```
### When NOT to Use
**Don't use this when:**
- Your type doesn't manage long-lived resources (a pure data struct, a stateless transformer)
- Shutdown order doesn't matter — a simple `Close()` suffices
- You're building a CLI tool that exits the process — `os.Exit` is your shutdown
**Over-application example:**
```go
// Graceful shutdown for a type that holds no connections
type Calculator struct {
precision int
}
func (c *Calculator) Shutdown(ctx context.Context) error {
// ... nothing to drain, nothing to close
return nil
}
```
**Better alternative:**
```go
// No shutdown needed — the GC handles it. Maybe a Reset() if you want to reuse.
type Calculator struct {
precision int
}
```
**Why:** The Close/Shutdown duality exists for types that own goroutines, connections, or
file descriptors that outlive individual method calls. If your type is just data and methods,
adding shutdown ceremony is over-engineering that confuses users into thinking there are
hidden resources to manage.
**Anti-pattern:** Only providing one shutdown mode; not accepting a context for
timeout control; leaking goroutines on shutdown.
**Code example from source:**
```go
// net/http/server.go:3171-3175
func (s *Server) Close() error {
s.inShutdown.Store(true)
s.mu.Lock()
defer s.mu.Unlock()
err := s.closeListenersLocked()
// ... forcefully closes all active connections
}
// net/http/server.go:3221+
// Shutdown gracefully shuts down the server without interrupting any
// active connections.
func (s *Server) Shutdown(ctx context.Context) error {
s.inShutdown.Store(true)
// ... closes listeners, waits for idle, respects ctx deadline
}
```
---
## 10. Channel-Based Timer/Ticker API
**Pattern name:** NewXxx Returning Channel-Bearing Struct
**Source citation:** [time/tick.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/tick.go#L16), [time/sleep.go#L89](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/sleep.go#L89)
**What it does:** `NewTicker(d)` and `NewTimer(d)` return structs with a `C <-chan Time`
field. Consumers select on the channel to receive time events.
**Why:** Integrates time-based events with Go's concurrency primitives (select).
The channel-based API composes naturally with other goroutine patterns.
**Anti-pattern:** Callback-based timer APIs that don't compose with select; exposing
the send side of the channel; not documenting goroutine safety.
**Code example from source:**
```go
// time/tick.go:16-18
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
initTicker bool
}
// time/tick.go:36-45
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic("non-positive interval for NewTicker")
}
c := make(chan Time, 1)
t := (*Ticker)(unsafe.Pointer(newTimer(when(d), int64(d), sendTime, c, syncTimer(c))))
t.C = c
return t
}
```
<!-- PATTERN_COMPLETE -->
+207 -451
View File
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
## 1. sync.Mutex — The Basic Lock
### Source: [src/sync/mutex.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L18), [src/sync/mutex.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L42)
### Source: `src/sync/mutex.go:18-34`, `src/sync/mutex.go:42-67`
```go
// [src/sync/mutex.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L18)
// src/sync/mutex.go:18-34
// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
@@ -19,16 +19,21 @@ type Mutex struct {
mu isync.Mutex
}
// [src/sync/mutex.go#L36](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L36)
// src/sync/mutex.go:36-39
type Locker interface {
Lock()
Unlock()
}
// [src/sync/mutex.go#L43](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L43)
// 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
@@ -38,85 +43,6 @@ func (m *Mutex) Lock() {
- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock
- **Locker interface** — abstracts over Mutex and RWMutex
### When to Use
**Triggers:**
- Multiple goroutines read AND write the same data structure
- You need to protect a small critical section (a few field accesses)
- A channel-based solution would add complexity without benefit (no coordination needed, just protection)
**Example — before:**
```go
type Stats struct {
hits int
misses int
}
func (s *Stats) RecordHit() { s.hits++ } // DATA RACE when called from multiple goroutines
func (s *Stats) RecordMiss() { s.misses++ } // DATA RACE
```
**Example — after:**
```go
type Stats struct {
mu sync.Mutex
hits int
misses int
}
func (s *Stats) RecordHit() {
s.mu.Lock()
defer s.mu.Unlock()
s.hits++
}
```
### When NOT to Use
**Don't use this when:**
- Goroutines need to coordinate or communicate (not just protect state) — use channels
- The critical section involves blocking I/O (holding a mutex during network calls starves other goroutines)
- You can restructure to have a single goroutine own the state (no sharing = no lock needed)
**Over-application example:**
```go
// Using a mutex to coordinate work between goroutines
type TaskQueue struct {
mu sync.Mutex
tasks []Task
ready bool
}
func (q *TaskQueue) WaitForReady() {
for {
q.mu.Lock()
if q.ready {
q.mu.Unlock()
return
}
q.mu.Unlock()
time.Sleep(10 * time.Millisecond) // spin-waiting with a lock — terrible
}
}
```
**Better alternative:**
```go
// Use a channel for coordination/signaling
type TaskQueue struct {
tasks chan Task
ready chan struct{}
}
func (q *TaskQueue) WaitForReady() {
<-q.ready // blocks efficiently, no spinning
}
```
**Why:** Mutexes protect data; channels coordinate goroutines. If you're polling a mutex-protected flag, you've reinvented a bad channel. The Go proverb applies: "Don't communicate by sharing memory; share memory by communicating."
### Idiomatic Usage
```go
@@ -134,11 +60,12 @@ type Config struct {
mu sync.Mutex
data map[string]string
}
c2 := *c1 // COPIES the mutex — data race
c2 := *c1 // COPIES the mutex — data race waiting to happen
// DON'T: Forget defer
mu.Lock()
doSomething() // if this panics, mutex stays locked forever
// if this panics, the mutex stays locked forever
doSomething()
mu.Unlock()
```
@@ -146,24 +73,24 @@ mu.Unlock()
## 2. sync.Once — Exactly-Once Initialization
### Source: [src/sync/once.go#L12](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L12), [src/sync/once.go#L56](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L56)
### Source: `src/sync/once.go:12-36`, `src/sync/once.go:56-79`
```go
// [src/sync/once.go#L12](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L12)
// src/sync/once.go:12-23
type Once struct {
_ noCopy
done atomic.Bool
m Mutex
}
// [src/sync/once.go#L56](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L56)
// src/sync/once.go:56-63
func (o *Once) Do(f func()) {
if !o.done.Load() {
o.doSlow(f)
}
}
// [src/sync/once.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L65)
// src/sync/once.go:65-72
func (o *Once) doSlow(f func()) {
o.m.Lock()
defer o.m.Unlock()
@@ -176,101 +103,10 @@ func (o *Once) doSlow(f func()) {
### 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 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).
### When to Use
**Triggers:**
- You have expensive initialization that should happen exactly once (DB connection, config parse, compiled regex)
- Multiple goroutines may trigger the initialization concurrently
- You're using `var` + `if instance == nil` checks that aren't goroutine-safe
**Example — before:**
```go
var db *sql.DB
func GetDB() *sql.DB {
if db == nil { // RACE: two goroutines can both see nil
db, _ = sql.Open("postgres", connStr)
}
return db
}
```
**Example — after:**
```go
var (
db *sql.DB
once sync.Once
)
func GetDB() *sql.DB {
once.Do(func() {
db, _ = sql.Open("postgres", connStr)
})
return db
}
```
### When NOT to Use
**Don't use this when:**
- Initialization can fail and you need to retry — Once runs the func at most once, even on failure
- You need to reset/reinitialize later (Once has no Reset method)
- The initialization is cheap — just do it in `init()` or at declaration time
**Over-application example:**
```go
var (
conn *grpc.ClientConn
once sync.Once
)
func GetConn() (*grpc.ClientConn, error) {
var err error
once.Do(func() {
conn, err = grpc.Dial("server:443")
})
if err != nil {
return nil, err // PROBLEM: next call to GetConn returns nil conn, nil err
// because once.Do won't run again
}
return conn, nil
}
```
**Better alternative:**
```go
// Use sync.OnceValues (Go 1.21+) which caches both value and error,
// or handle retry logic explicitly
var getConn = sync.OnceValues(func() (*grpc.ClientConn, error) {
return grpc.Dial("server:443")
})
// Or for retry scenarios, use a mutex with a nil check
var (
mu sync.Mutex
conn *grpc.ClientConn
)
func GetConn() (*grpc.ClientConn, error) {
mu.Lock()
defer mu.Unlock()
if conn != nil {
return conn, nil
}
var err error
conn, err = grpc.Dial("server:443") // retries on next call if this fails
return conn, err
}
```
**Why:** `sync.Once` guarantees exactly-once execution regardless of success or failure. If the initialization can fail transiently (network timeout, service unavailable), Once will permanently cache the failure. Use `sync.OnceValues` for caching results, or a mutex with a nil-check pattern when retry is needed.
### Idiomatic Usage
```go
@@ -290,6 +126,18 @@ func GetDB() *DB {
### 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() {
@@ -301,12 +149,16 @@ once.Do(func() {
## 3. sync.WaitGroup — Waiting for Goroutine Completion
### Source: [src/sync/waitgroup.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L14), [src/sync/waitgroup.go#L236](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L236)
### Source: `src/sync/waitgroup.go:14-43`, `src/sync/waitgroup.go:236-260`
```go
// [src/sync/waitgroup.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L14)
// Typically, a main goroutine will start tasks by calling WaitGroup.Go
// and then wait for all tasks to complete by calling WaitGroup.Wait:
// 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)
@@ -322,14 +174,13 @@ type WaitGroup struct {
### Go 1.25+: WaitGroup.Go
```go
// [src/sync/waitgroup.go#L236](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L236)
// 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)
panic(x) // don't call Done — let panic propagate
}
wg.Done()
}()
@@ -340,7 +191,7 @@ func (wg *WaitGroup) Go(f func()) {
### 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.
`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)
@@ -368,27 +219,33 @@ for _, item := range items {
}()
}
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#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
### Source: `src/sync/pool.go:44-63`
```go
// [src/sync/pool.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
// 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. 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.
// relieving pressure on the garbage collector.
type Pool struct {
noCopy noCopy
local unsafe.Pointer
@@ -401,7 +258,7 @@ type Pool struct {
### Why
Pool is **not** a general cache. Items can vanish between GC cycles. Use for reducing allocation pressure on hot paths.
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)
@@ -429,28 +286,33 @@ func (p *pp) free() {
var connPool = sync.Pool{
New: func() any { return connectToDB() },
}
// Connections may be GC'd — use database/sql's pool instead
// 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
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#L83](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L83) (Done channel), [src/io/pipe.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L42)
### Source: `src/context/context.go:83-100` (Done channel), `src/io/pipe.go:42-45`
```go
// [src/context/context.go#L83](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L83)
// 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#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L42)
// 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
}
```
@@ -467,113 +329,14 @@ case result := <-work:
}
```
### When to Use
**Triggers:**
- You need to broadcast "stop" to multiple goroutines simultaneously
- A goroutine needs to select between work and cancellation
- You're implementing graceful shutdown for a long-running service
**Example — before:**
```go
type Server struct {
stopped bool // RACE: no synchronization
}
func (s *Server) worker() {
for {
if s.stopped { return } // busy-polls, racy
doWork()
}
}
```
**Example — after:**
```go
type Server struct {
done chan struct{}
}
func NewServer() *Server {
return &Server{done: make(chan struct{})}
}
func (s *Server) worker() {
for {
select {
case <-s.done:
return
case work := <-s.workCh:
process(work)
}
}
}
func (s *Server) Stop() { close(s.done) } // broadcasts to ALL workers
```
### When NOT to Use
**Don't use this when:**
- You need to send actual data between goroutines — use a typed channel
- You only have one goroutine to signal — a simple `return` or function call suffices
- You're using it as a poor substitute for `context.Context` (which already provides Done())
**Over-application example:**
```go
// Rolling your own done channel when context already provides this
type Worker struct {
done chan struct{}
ctx context.Context
cancel context.CancelFunc
}
func (w *Worker) Start() {
go func() {
select {
case <-w.done: // redundant — ctx.Done() does the same thing
return
case <-w.ctx.Done():
return
}
}()
}
```
**Better alternative:**
```go
// Just use the context — it already IS a done signal
type Worker struct {
ctx context.Context
cancel context.CancelFunc
}
func (w *Worker) Start() {
go func() {
select {
case <-w.ctx.Done():
return
case work := <-w.workCh:
process(work)
}
}()
}
func (w *Worker) Stop() { w.cancel() }
```
**Why:** If you already have a `context.Context`, its `Done()` channel is your cancellation signal. Adding a separate `chan struct{}` duplicates functionality and creates two shutdown paths that must be kept in sync. Use raw done channels only when you don't have a context (e.g., standalone libraries that predate context).
### Anti-pattern
```go
// DON'T: Use chan bool for done signals
done := make(chan bool) // wastes 1 byte, true/false meaningless
done := make(chan bool) // wastes 1 byte per signal, true/false meaningless
// DON'T: Send to done (only unblocks one receiver)
done <- struct{}{}
// 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)
@@ -581,12 +344,15 @@ close(done)
---
## 6. Context Propagation Rules
## 6. Context Propagation
### Source: [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
### Source: `src/context/context.go:37-48` (rules), `src/net/http/request.go:368-380`
From the package doc:
```go
// [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
// 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:
@@ -599,19 +365,34 @@ close(done)
// 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. It carries deadlines and cancellation signals for the current request, not persistent state.
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
ctx context.Context // stale context persists beyond request lifecycle
}
// DON'T: Pass nil
// DON'T: Pass nil context
doWork(nil, data) // use context.TODO() if unsure
// DON'T: Put context anywhere other than first parameter
@@ -620,65 +401,64 @@ func doWork(data Data, ctx context.Context) // wrong position
---
## 7. Context Cancellation with Timeout
## 7. Context Cancellation (WithCancel/WithTimeout)
### Source: [src/net/http/server.go#L4007](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4007) (TimeoutHandler)
### Source: `src/context/context.go:242-249` (WithCancel), `src/net/http/server.go:4007-4050` (TimeoutHandler)
```go
// [src/net/http/server.go#L4011](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4011)
// 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{})
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
// handler completed
case <-ctx.Done():
// timeout — write 503
// timeout
}
}
```
### 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
`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 timer goroutines)
ctx, _ := context.WithTimeout(parent, 5*time.Second)
// DON'T: Forget to call cancel (leaks goroutines)
ctx, _ := context.WithCancel(parent) // cancel function discarded!
// DON'T: Ignore context in long operations
func longWork(ctx context.Context) {
time.Sleep(10 * time.Minute) // ignores cancellation
}
// 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 Non-Blocking Check
## 8. Select with Done Channel
### Source: [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
### Source: `src/context/context.go:83-100` (Done in select), `src/io/pipe.go:51-60`
```go
// [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
// src/io/pipe.go:51-60
func (p *pipe) read(b []byte) (n int, err error) {
select {
case <-p.done:
@@ -699,7 +479,26 @@ func (p *pipe) read(b []byte) (n int, err error) {
### 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.
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
@@ -711,29 +510,71 @@ for {
return
default:
}
// busy-spins CPU at 100%!
// busy-spins CPU at 100%
}
```
---
## 9. Channel Pipeline (io.Pipe)
## 9. Goroutine-per-Connection (net/http Server)
### Source: [src/io/pipe.go#L38](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L38), [src/io/pipe.go#L195](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L195)
### Source: `src/net/http/server.go` (conceptual — the serve loop spawns goroutines per connection)
```go
// [src/io/pipe.go#L38](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L38)
// 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#L195](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L195)
// 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
wrCh: make(chan []byte),
rdCh: make(chan int),
done: make(chan struct{}),
}}}
return &pw.r, pw
@@ -742,9 +583,9 @@ func Pipe() (*PipeReader, *PipeWriter) {
### Why
`io.Pipe` uses **unbuffered channels** — each Write blocks until Read consumes. Backpressure is automatic. The `done` channel signals shutdown.
`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.
### Pipeline Pattern Template
### Pattern: Channel Pipeline
```go
func generate(ctx context.Context) <-chan int {
@@ -763,91 +604,6 @@ func generate(ctx context.Context) <-chan int {
}
```
### When to Use
**Triggers:**
- You have a producer-consumer flow where the consumer's speed should limit the producer (backpressure)
- Data flows through multiple transformation stages
- You want to decouple stages that can run concurrently
**Example — before:**
```go
func processAll(items []string) []Result {
var results []Result
for _, item := range items {
fetched := fetch(item) // sequential: fetch then transform
results = append(results, transform(fetched))
}
return results
}
```
**Example — after:**
```go
func processAll(ctx context.Context, items []string) []Result {
fetched := make(chan Fetched)
go func() {
defer close(fetched)
for _, item := range items {
select {
case fetched <- fetch(item): // backpressure: blocks if transform is slow
case <-ctx.Done():
return
}
}
}()
var results []Result
for f := range fetched {
results = append(results, transform(f))
}
return results
}
```
### When NOT to Use
**Don't use this when:**
- The "pipeline" has only one stage — you're just adding goroutine overhead to sequential code
- All items are already in memory and processing is CPU-bound with no I/O — `for` loop is simpler and faster
- You don't actually need backpressure (data fits in memory, producer is finite)
**Over-application example:**
```go
// Channel pipeline for a simple in-memory transformation
func doubleAll(nums []int) []int {
ch := make(chan int)
go func() {
defer close(ch)
for _, n := range nums {
ch <- n // channel overhead for no benefit
}
}()
var result []int
for n := range ch {
result = append(result, n*2)
}
return result
}
```
**Better alternative:**
```go
// Just use a loop — no concurrency needed
func doubleAll(nums []int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = n * 2
}
return result
}
```
**Why:** Channel pipelines shine when stages involve I/O (network, disk) and can overlap waiting. For pure computation on in-memory data, the channel send/receive overhead (~50-100ns per item) adds up and the goroutine scheduling has no useful work to overlap with. A plain loop is faster, simpler, and easier to debug.
### Anti-pattern
```go
@@ -858,7 +614,7 @@ func produce() <-chan int {
for i := 0; i < 10; i++ {
ch <- i
}
// forgot close(ch) — range receivers hang
// forgot close(ch) — receivers hang on range
}()
return ch
}
@@ -866,17 +622,18 @@ func produce() <-chan int {
---
## 10. Background Worker with Context Shutdown
## 11. database/sql Connection Opener Goroutine
### Source: [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
### Source: `src/database/sql/sql.go:836-843`
```go
// [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
// 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)
@@ -886,27 +643,27 @@ func OpenDB(c driver.Connector) *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.
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 goroutines without shutdown mechanism
// DON'T: Start background goroutines without shutdown mechanism
go func() {
for {
processWork() // runs forever, no way to stop
processWork() // runs forever, no way to stop it
}
}()
```
---
## 11. noCopy — Preventing Value Copies
## 12. noCopy — Preventing Value Copies at Vet Time
### Source: [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
### Source: `src/sync/cond.go:120-126`
```go
// [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
// src/sync/cond.go:120-126
type noCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
@@ -916,14 +673,14 @@ 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.
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!
defer wg.Done() // operates on copy, not original
func doWork(wg sync.WaitGroup) { // copies the WaitGroup!
defer wg.Done()
}
// DO: Pass by pointer
@@ -945,7 +702,6 @@ func doWork(wg *sync.WaitGroup) {
| 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 |
<!-- PATTERN_COMPLETE -->
-989
View File
@@ -1,989 +0,0 @@
# Go Configuration Patterns
Patterns for configuring Go types and services, extracted from the
Go standard library source.
**Source:** [golang/go](https://github.com/golang/go) at commit
[`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
**Stats:** 33 Config/Options structs, 20 `With*` functions, 14
`Default*` exports, 9 `Register*` functions in public stdlib.
---
## 1. Zero-Value Usable Config Structs
Struct with sensible defaults when all fields are zero. Users only
set what they need to change.
### Source:
[crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566)
```go
// src/crypto/tls/common.go:566
type Config struct {
// Rand provides the source of entropy for nonces and RSA blinding.
// If Rand is nil, TLS uses the cryptographic random reader in package
// crypto/rand.
Rand io.Reader
// Time returns the current time as the number of seconds since the epoch.
// If Time is nil, TLS uses time.Now.
Time func() time.Time
// Certificates contains one or more certificate chains to present to the
// other side of the connection.
Certificates []Certificate
// ...
}
```
Every field documents its zero-value behavior: "If nil, uses X."
The entire struct can be used as `&tls.Config{}` and it works.
### Why
Users only think about what they're changing. The stdlib handles
defaults internally. This eliminates "forgot to set field X" bugs
and makes constructor boilerplate unnecessary for simple cases.
### When to Use
**Triggers:**
- You're configuring a long-lived object (server, client, handler)
- Most users will only change 1-3 fields
- Each field has an obvious sensible default
- The struct will grow over time (backward compatibility via zero values)
**Example — before:**
```go
// Without zero-value defaults — every user must know about every field
func NewServer(addr string, handler http.Handler, readTimeout time.Duration,
writeTimeout time.Duration, maxHeaderBytes int, tlsConfig *tls.Config,
errorLog *log.Logger) *Server {
return &Server{
Addr: addr, Handler: handler, ReadTimeout: readTimeout,
WriteTimeout: writeTimeout, MaxHeaderBytes: maxHeaderBytes,
TLSConfig: tlsConfig, ErrorLog: errorLog,
}
}
// Caller must specify everything:
s := NewServer(":8080", mux, 30*time.Second, 30*time.Second, 1<<20, nil, nil)
```
**Example — after:**
```go
// With zero-value usable struct — users set only what they care about
s := &http.Server{
Addr: ":8080",
Handler: mux,
}
// ReadTimeout, WriteTimeout, MaxHeaderBytes all have documented defaults.
// TLSConfig, ErrorLog use stdlib defaults when nil.
```
### When NOT to Use
**Don't use this when:**
- There is no sensible default for a field (e.g., a database
connection string — there's no "default" database)
- The zero value is dangerous (e.g., `Timeout: 0` meaning "no
timeout" when you WANT a timeout by default)
- Users MUST make a conscious choice (use a constructor that
forces the required parameters)
**Over-application example:**
```go
// Bad: zero value is DANGEROUS
type RetryConfig struct {
MaxRetries int // zero = no retries? or infinite retries?
Timeout time.Duration // zero = no timeout = hang forever
}
// User creates: &RetryConfig{} — is that safe? Nobody knows.
```
**Better alternative:**
```go
// Required parameters in constructor, optional in struct
func NewRetrier(maxRetries int, timeout time.Duration) *Retrier {
if maxRetries <= 0 {
panic("maxRetries must be positive")
}
if timeout <= 0 {
panic("timeout must be positive")
}
return &Retrier{maxRetries: maxRetries, timeout: timeout}
}
```
### Anti-pattern
```go
// DON'T: Config struct with fields that mean different things at zero
type Config struct {
Port int // 0 = random port? or invalid? or default 8080?
}
// DO: Document and handle zero explicitly
type Config struct {
// Port specifies the TCP port to listen on.
// If zero, defaults to 8080.
Port int
}
```
---
## 2. Options Struct as Function Parameter
Pass an exported struct of optional settings to a constructor or
method.
### Source:
[log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135)
```go
// src/log/slog/handler.go:135
type HandlerOptions struct {
// AddSource causes the handler to compute the source code position
// of the log statement and add a SourceKey attribute to the output.
AddSource bool
// Level reports the minimum record level that will be logged.
// The handler discards records with lower levels.
// If Level is nil, the handler assumes LevelInfo.
Level Leveler
// ReplaceAttr is called to rewrite each non-group attribute
// before it is logged.
ReplaceAttr func(groups []string, a Attr) Attr
}
```
Usage:
```go
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
Level: slog.LevelDebug,
})
```
### Why
Separates required arguments (the writer) from optional configuration.
The struct can be `nil` (use all defaults) or partially filled.
Adding fields later doesn't break callers.
### When to Use
**Triggers:**
- You have 3+ optional parameters for a constructor
- Parameters are related and configure the same subsystem
- Users will often use defaults for most of them
- You need to be able to add options without breaking callers
**Example — before:**
```go
// Growing parameter list — every new option breaks all callers
func NewHandler(w io.Writer, addSource bool, level Level,
replaceAttr func([]string, Attr) Attr) *Handler { ... }
// Every caller must pass all args even for defaults:
h := NewHandler(os.Stdout, false, LevelInfo, nil)
```
**Example — after:**
```go
// Options struct — nil means "all defaults"
func NewHandler(w io.Writer, opts *HandlerOptions) *Handler { ... }
// Simple case — no options needed:
h := slog.NewJSONHandler(os.Stdout, nil)
// Custom case — only set what you need:
h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
```
### When NOT to Use
**Don't use this when:**
- You have 1-2 optional parameters (just use direct params with
zero-value semantics)
- The options change per-call, not per-instance (use functional
options for per-call variation)
- Every user needs different options (nothing is truly "optional")
**Over-application example:**
```go
// Unnecessary: only one option, and it's always set
type ParseOptions struct {
Format string
}
func Parse(input string, opts *ParseOptions) (*Result, error) {
format := "json" // default
if opts != nil {
format = opts.Format
}
// ...
}
// Every caller writes: Parse(input, &ParseOptions{Format: "yaml"})
// This is MORE awkward than: Parse(input, "yaml")
```
**Better alternative:**
```go
// When there's really only one option, make it a parameter:
func Parse(input string, format string) (*Result, error) { ... }
```
---
## 3. Functional Options (With* Pattern)
Functions that return an opaque Options type, composed via variadic
parameters.
### Source:
[encoding/json/jsontext/options.go#L232](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/jsontext/options.go#L232)
```go
// src/encoding/json/jsontext/options.go:232
func WithIndent(indent string) Options {
// ...
}
// src/encoding/json/jsontext/encode.go:91
func NewEncoder(w io.Writer, opts ...Options) *Encoder {
e := new(Encoder)
e.Reset(w, opts...)
return e
}
```
Usage:
```go
enc := jsontext.NewEncoder(w,
jsontext.WithIndent(" "),
jsontext.WithByteLimit(1024*1024),
)
```
### Why
Options can be added over time without breaking callers. Each option
is self-documenting (the function name says what it does). Options
can be pre-composed and reused. The zero-option case reads cleanly:
`NewEncoder(w)`.
### When to Use
**Triggers:**
- The option set will grow over time (new features, new modes)
- Options should be individually composable and reusable
- Per-call configuration (not just per-instance)
- You want option names in the API surface (not struct field names)
**Example — before:**
```go
// Options struct works but gets unwieldy with many fields:
enc := json.NewEncoder(w, &json.EncoderOptions{
Indent: " ",
ByteLimit: 1024*1024,
DepthLimit: 100,
EscapeHTML: true,
SortMapKeys: true,
})
```
**Example — after:**
```go
// Functional options — compose only what you need:
enc := json.NewEncoder(w,
json.WithIndent(" "),
json.WithByteLimit(1024*1024),
)
// Pre-compose for reuse:
var prettyJSON = []json.Options{
json.WithIndent(" "),
json.WithByteLimit(10*1024*1024),
}
enc := json.NewEncoder(w, prettyJSON...)
```
### When NOT to Use
**Don't use this when:**
- You have <3 options that won't grow (use direct parameters or
an options struct)
- Options interact with each other in complex ways (a struct makes
dependencies visible; functional options hide them)
- Users need to inspect/read back the configuration (options are
typically write-only — you can set them but not query them)
**Over-application example:**
```go
// Overkill for 2 stable options:
func Connect(addr string, opts ...ConnectOption) (*Conn, error)
// Every caller writes:
conn, _ := Connect("localhost:5432",
WithTimeout(5*time.Second),
WithTLS(true),
)
// vs simply:
conn, _ := Connect("localhost:5432", 5*time.Second, true)
// or:
conn, _ := Connect("localhost:5432", &ConnectOptions{
Timeout: 5*time.Second, TLS: true,
})
```
**Better alternative:** Use an options struct when:
- The option set is stable (<5 options)
- Users need to read options back
- Options interact (struct makes co-dependencies visible)
---
## 4. Package-Level Default Instances
A package provides a pre-configured, ready-to-use instance.
### Source:
[net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L109)
```go
// src/net/http/client.go:109
var DefaultClient = &Client{}
// src/net/http/transport.go:47
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// src/log/slog/logger.go:55
func Default() *Logger { return defaultLogger.Load() }
```
### Why
Most programs need exactly one instance with default settings. The
package-level default eliminates boilerplate for the common case
while still allowing custom instances for tests or specialized needs.
### When to Use
**Triggers:**
- 90% of users will use the default configuration
- The type is safe for concurrent use
- Creating an instance requires non-trivial setup (transport pools,
connection config)
- Package-level functions exist that delegate to the default
**Example — before:**
```go
// Without default — every caller must create and configure
client := &http.Client{
Transport: &http.Transport{
MaxIdleConns: 100,
// ... 10 more fields for reasonable defaults
},
}
resp, err := client.Get(url)
```
**Example — after:**
```go
// With package-level default — simple case is one line
resp, err := http.Get(url) // uses http.DefaultClient internally
```
### When NOT to Use
**Don't use this when:**
- Every user needs different configuration (no meaningful default)
- The instance holds resources that should be explicitly closed
- Global mutable state would cause test interference
- The type is NOT safe for concurrent use
**Over-application example:**
```go
// Bad: default database connection — there IS no universal default
var DefaultDB = MustConnect("postgres://localhost/mydb")
// What database? What credentials? This makes no sense as a default.
```
**Better alternative:**
```go
// Force users to be explicit about connections
db, err := sql.Open("postgres", connString)
```
### Anti-pattern
```go
// DON'T: Mutable default that tests can't isolate
var DefaultLogger = NewLogger(os.Stdout)
// Tests that modify DefaultLogger race with each other
// DO: Immutable default with replacement via function
func Default() *Logger { return defaultLogger.Load() }
func SetDefault(l *Logger) { defaultLogger.Store(l) }
// Atomic replacement — tests can use SetDefault safely
```
---
## 5. Init-Time Registration
Plugins/drivers register themselves in `init()`, looked up at runtime
by name.
### Source:
[database/sql/sql.go#L53](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L53)
```go
// src/database/sql/sql.go:53
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("sql: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
drivers[name] = driver
}
```
Driver packages register in init:
```go
// In the driver package (e.g., github.com/lib/pq):
func init() {
sql.Register("postgres", &Driver{})
}
```
Users import for side effects:
```go
import (
"database/sql"
_ "github.com/lib/pq" // registers "postgres" driver
)
db, err := sql.Open("postgres", connString)
```
### Why
Decouples the framework from implementations. The `database/sql`
package doesn't import any driver — drivers import IT and register.
New drivers can be added without changing the framework.
### When to Use
**Triggers:**
- You're building a framework/registry with pluggable backends
- Implementations are in separate packages (compile-time decoupling)
- Users choose implementations at link time (import selection)
- The set of implementations is open-ended
**Example — before:**
```go
// Hard-coded implementations — every new driver requires editing this
func Open(driverName, dataSourceName string) (*DB, error) {
switch driverName {
case "postgres":
return openPostgres(dataSourceName)
case "mysql":
return openMySQL(dataSourceName)
// Can't add new drivers without modifying this code
}
}
```
**Example — after:**
```go
// Registration pattern — open to extension, closed to modification
func Open(driverName, dataSourceName string) (*DB, error) {
driver, ok := drivers[driverName]
if !ok {
return nil, fmt.Errorf("sql: unknown driver %q", driverName)
}
return driver.Open(dataSourceName)
}
// New drivers register themselves — zero changes to this code.
```
### When NOT to Use
**Don't use this when:**
- You control all implementations (use interfaces directly)
- Registration order matters (init order is non-deterministic across
packages)
- You need to test without global state pollution
- The "plugin" needs configuration beyond just existing
**Over-application example:**
```go
// Over-engineered for an internal app with 2 known implementations
var handlers = map[string]Handler{}
func Register(name string, h Handler) { handlers[name] = h }
func init() { Register("json", &JSONHandler{}) }
func init() { Register("xml", &XMLHandler{}) }
// These are always the same two. Just use a constructor:
func NewHandler(format string) Handler {
switch format {
case "json": return &JSONHandler{}
case "xml": return &XMLHandler{}
}
}
```
### Anti-pattern
```go
// DON'T: Registration without duplicate detection
func Register(name string, d Driver) {
drivers[name] = d // silently overwrites — last-import-wins
}
// DO: Panic on duplicate (from database/sql)
if _, dup := drivers[name]; dup {
panic("sql: Register called twice for driver " + name)
}
```
---
## 6. Context-Carried Configuration (WithValue)
Request-scoped configuration passed through context.Context.
### Source:
[context/context.go#L728](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L728)
```go
// src/context/context.go:728
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
```
Usage with unexported key type (the idiom):
```go
// src/net/http/httptrace/trace.go:33
type clientEventContextKey struct{}
func WithClientTrace(ctx context.Context, trace *ClientTrace) context.Context {
// ...
return context.WithValue(ctx, clientEventContextKey{}, trace)
}
```
### Why
Passes request-scoped data through call chains without adding
parameters to every function signature. The unexported key type
prevents collision between packages.
### When to Use
**Triggers:**
- Data is request-scoped (trace ID, auth token, deadline)
- Data must cross package boundaries without coupling them
- The data is "ambient" (needed by middleware/infrastructure, not
business logic)
- Adding a parameter to every function in the chain is impractical
**Example — before:**
```go
// Propagating trace ID through 5 layers of function calls:
func HandleRequest(traceID string, r *Request) {
result := processOrder(traceID, r.Order)
notify(traceID, result)
}
func processOrder(traceID string, o Order) Result {
validated := validate(traceID, o)
return persist(traceID, validated)
}
// traceID is threaded through EVERY function — pollutes all signatures
```
**Example — after:**
```go
type traceIDKey struct{}
func WithTraceID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, traceIDKey{}, id)
}
func HandleRequest(ctx context.Context, r *Request) {
result := processOrder(ctx, r.Order)
notify(ctx, result)
}
// traceID travels invisibly in ctx — only extracted where needed
```
### When NOT to Use
**Don't use this when:**
- The data is required for correctness (make it an explicit param —
context values are invisible, easy to forget)
- The data is needed in EVERY function (it's not ambient, it's core)
- You're using it to avoid adding a parameter to 2-3 functions
(that's not enough pain to justify the indirection)
- The data is mutable (context values are immutable by convention)
**Over-application example:**
```go
// Bad: config that EVERY function needs — should be a field
type serverConfig struct{}
func handleRequest(ctx context.Context, r *Request) {
cfg := ctx.Value(serverConfig{}).(*Config)
// Every handler digs into context for core config
// This is just dependency injection with extra steps and no type safety
}
```
**Better alternative:**
```go
// Make it a field on the server/handler:
type Server struct {
config *Config
}
func (s *Server) handleRequest(r *Request) {
// s.config is always there, typed, visible
}
```
### Anti-pattern
```go
// DON'T: Exported key type (allows collision)
var TraceKey = "trace-id" // any package can use this string
// DO: Unexported struct type (package-scoped, collision-proof)
type traceKey struct{}
```
---
## 7. Builder Pattern via Method Chaining
Not a stdlib pattern — but its ABSENCE is instructive.
### Source:
The Go stdlib does NOT use builder patterns. Zero instances of
method chaining for configuration exist in the public API.
### Why
Go prefers struct literals for construction. Builders hide what's
being set, make types non-trivially copyable, and create an
awkward "build phase" vs "use phase" distinction.
### When to Use
**Almost never in Go.** The only legitimate case:
- Building immutable objects where the construction process is
genuinely complex (>10 steps with conditionals)
- Even then, prefer a config struct + constructor.
### When NOT to Use
**Don't use this when:**
- A struct literal works (always try struct literal first)
- You're porting patterns from Java/C# (they use builders because
they lack struct literals with named fields)
- You want "fluent" APIs (Go culture values explicit over clever)
**Over-application example:**
```go
// Java-brain in Go:
server := NewServerBuilder().
WithAddr(":8080").
WithTimeout(30 * time.Second).
WithHandler(mux).
WithTLS(cert, key).
Build()
// In Go, this is just:
server := &http.Server{
Addr: ":8080",
ReadTimeout: 30 * time.Second,
Handler: mux,
TLSConfig: tlsConfig,
}
// Clearer, no hidden state, no build/use phase split.
```
---
## 8. Exported Fields with Documented Nil Behavior
Config fields that accept function values, with nil meaning "use
default behavior."
### Source:
[crypto/tls/common.go#L575](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L575)
```go
// src/crypto/tls/common.go:575
// Time returns the current time as the number of seconds since the epoch.
// If Time is nil, TLS uses time.Now.
Time func() time.Time
```
Also: [log/slog/handler.go#L169](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L169)
```go
// ReplaceAttr is called to rewrite each non-group attribute before
// it is logged. If ReplaceAttr returns a zero Attr, the attribute
// is discarded.
ReplaceAttr func(groups []string, a Attr) Attr
```
### Why
Allows injection of custom behavior without requiring interfaces.
The nil check is simpler than defining an interface, implementing a
default, and wiring it. Good for hooks where most users want the
default but advanced users need to customize.
### When to Use
**Triggers:**
- Optional behavior customization (not every user needs it)
- The "interface" would have exactly one method
- Default behavior is obvious (time.Now, os.Stderr, etc.)
- Function signature is stable
**Example — before:**
```go
// Interface for one method — ceremony for no gain
type TimeProvider interface {
Now() time.Time
}
type defaultTimeProvider struct{}
func (d defaultTimeProvider) Now() time.Time { return time.Now() }
type Config struct {
TimeProvider TimeProvider // required interface, must set
}
```
**Example — after:**
```go
// Function field — nil means default
type Config struct {
// Time returns the current time.
// If nil, time.Now is used.
Time func() time.Time
}
// Usage in implementation:
func (c *Config) now() time.Time {
if c.Time != nil {
return c.Time()
}
return time.Now()
}
```
### When NOT to Use
**Don't use this when:**
- The function has side effects that need lifecycle management
(use an interface with Close)
- Multiple methods are needed together (use an interface)
- The function needs to carry state (use a struct implementing
an interface)
---
## 9. Immutable-After-Use Convention
Config structs that must not be modified after being passed to a
constructor.
### Source:
[crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566)
```go
// A Config structure is used to configure a TLS client or server.
// After one has been passed to a TLS function it must not be modified.
// A Config may be reused; the tls package will also not modify it.
type Config struct { ... }
```
### Why
Avoids defensive copying of large structs. The TLS config has 30+
fields — copying on every handshake would waste memory. Instead,
the contract is: "you give it to us, you stop touching it."
### When to Use
**Triggers:**
- Config struct is large (>5 fields)
- Copying would be expensive (contains slices, maps, or pointers)
- The configured object is long-lived (server, pool, transport)
- Concurrent access to config is possible
**Example — before:**
```go
// Defensive copy — safe but expensive for large configs
func NewServer(cfg Config) *Server {
cfgCopy := cfg // copies all fields
return &Server{config: &cfgCopy}
}
```
**Example — after:**
```go
// Document immutability constraint — no copy needed
// "After one has been passed to NewServer it must not be modified."
func NewServer(cfg *Config) *Server {
return &Server{config: cfg} // shared reference, caller must not mutate
}
```
### When NOT to Use
**Don't use this when:**
- The struct is small and cheap to copy (just copy it)
- Users frequently need to create variations (provide a `Clone()`
method instead)
- The contract is hard to enforce (tests can't catch violations)
---
## 10. Clone for Config Variation
Provide a `Clone()` method when users need to create modified copies
of immutable-after-use configs.
### Source:
[crypto/tls/common.go#L996](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L996) (tls.Config.Clone)
```go
// Clone returns a shallow clone of c or nil if c is nil. It is safe to clone
// a Config that is being used concurrently by a TLS client or server.
func (c *Config) Clone() *Config {
if c == nil {
return nil
}
c.mutex.Lock()
defer c.mutex.Unlock()
return &Config{
Rand: c.Rand,
Time: c.Time,
Certificates: c.Certificates,
// ... all fields copied
}
}
```
### Why
When a config is immutable-after-use but users need variations
(e.g., same TLS config but different ServerName for each host),
Clone gives them a safe way to fork without modifying the original.
### When to Use
**Triggers:**
- You have immutable-after-use config structs
- Users need slight variations of a base config
- The struct contains reference types (slices, maps) that need
safe copying
- The struct has unexported fields or a mutex
**Example — before:**
```go
// Without Clone — users attempt (broken) manual copy
baseCfg := &tls.Config{MinVersion: tls.VersionTLS12}
// Can't just: hostCfg := *baseCfg (unexported fields, shared slices)
```
**Example — after:**
```go
baseCfg := &tls.Config{MinVersion: tls.VersionTLS12}
hostCfg := baseCfg.Clone()
hostCfg.ServerName = "example.com"
```
### When NOT to Use
**Don't use this when:**
- The struct has no unexported fields and no reference types
(plain struct copy `*s` works fine)
- Users rarely need variations (one config for the whole app)
---
## Summary: Configuration Decision Tree
```
Is the configuration per-instance or per-call?
├── Per-instance → struct-based patterns (#1, #2, #8, #9, #10)
│ ├── <3 options? → Direct parameters
│ ├── 3-10 options, stable? → Options struct (#2)
│ ├── Long-lived, most fields have defaults? → Zero-value config (#1)
│ ├── Must not mutate after use? → Immutable convention (#9) + Clone (#10)
│ └── Hook/callback injection? → Function fields (#8)
├── Per-call → functional patterns (#3, #6)
│ ├── Options will grow? → Functional options / With* (#3)
│ └── Request-scoped ambient data? → Context values (#6)
└── Framework/plugin boundary? → Registration (#5)
└── Global default for common case? → Default instance (#4)
```
**Key principle:** Start with the simplest pattern that works. Only
reach for functional options or registration when you have evidence
the option set is growing or implementations are external.
See also:
- [interfaces.md](interfaces.md) — Accept interfaces, return structs
- [api-conventions.md](api-conventions.md) — Backward compatibility
<!-- PATTERN_COMPLETE -->
-460
View File
@@ -1,460 +0,0 @@
# Documentation Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Package Documentation (doc.go or Package Comment)
**Pattern name:** Package Doc Comment
**Source citation:** [net/http/doc.go#L6](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/doc.go#L6), [os/file.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L5), [log/slog/doc.go#L6](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/doc.go#L6)
**What it does:** The first file in a package (by convention `doc.go`, or the main
source file) starts with a `// Package xxx ...` comment that explains the package's
purpose, key types, and typical usage patterns.
**Why:** This is the first thing users see in `go doc <pkg>` and on pkg.go.dev. It
sets context, teaches the mental model, and provides copy-paste examples.
**Anti-pattern:** No package comment; package comment that just restates the package
name ("Package http provides http"); putting documentation in README instead of code.
**Code examples from source:**
```go
// net/http/doc.go:6-12
/*
Package http provides HTTP client and server implementations.
[Get], [Head], [Post], and [PostForm] make HTTP (or HTTPS) requests:
resp, err := http.Get("http://example.com/")
...
*/
```
```go
// os/file.go:5-43
// Package os provides a platform-independent interface to operating system
// functionality. The design is Unix-like, although the error handling is
// Go-like; failing calls return values of type error rather than error numbers.
// Often, more information is available within the error. For example,
// if a call that takes a file name fails, such as [Open] or [Stat], the error
// will include the failing file name when printed and will be of type
// [*PathError], which may be unpacked for more information.
```
```go
// log/slog/doc.go:6-10
/*
Package slog provides structured logging,
in which log records include a message,
a severity level, and various other attributes
expressed as key-value pairs.
*/
```
---
## 2. Section Headers in Package Docs
**Pattern name:** `# Heading` in Doc Comments
**Source citation:** [os/file.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L37), `net/http/doc.go` (multiple sections)
**What it does:** Uses `# Section Name` within the package doc comment to organize
long documentation into navigable sections.
**Why:** Large packages need structure. Section headers render as links in pkg.go.dev
and provide a scannable table of contents.
**Anti-pattern:** Wall-of-text package docs; using `===` or `---` (not recognized);
too many sections (fragmenting simple docs).
**Code example from source:**
```go
// os/file.go:37
// # Concurrency
//
// The methods of [File] correspond to file system operations. All are
// safe for concurrent use.
```
---
## 3. Type/Function Comment Convention
**Pattern name:** `// TypeName verb...` or `// FuncName verb...`
**Source citation:** [net/http/server.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L65) (Handler), [bufio/scan.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L14) (Scanner)
**What it does:** Every exported identifier's doc comment starts with the identifier
name, followed by a verb phrase describing what it does or represents.
**Why:** `go doc` extracts the first sentence as a summary. Starting with the name
ensures it reads correctly in both isolation (summary lists) and full context.
This is enforced by convention and checked by linters.
**Anti-pattern:** Starting with "This function..." or "The Foo type..."; starting
with articles ("A Handler is...") for functions (acceptable for types); omitting
the comment entirely.
**Code examples from source:**
```go
// net/http/server.go:65
// A Handler responds to an HTTP request.
// bufio/scan.go:14-17
// Scanner provides a convenient interface for reading data such as
// a file of newline-delimited lines of text.
// net/http/request.go:867
// NewRequest wraps NewRequestWithContext using context.Background.
// os/file.go:389-390
// Open opens the named file for reading.
// regexp/regexp.go:310-312
// MustCompile is like [Compile] but panics if the expression cannot be parsed.
// It simplifies safe initialization of global variables holding compiled regular
// expressions.
```
---
## 4. Doc Links (Square Bracket References)
**Pattern name:** `[TypeName]`, `[Package.Symbol]`, `[Method]` Links
**Source citation:** [net/http/server.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L65), [os/file.go#L9](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L9)
**What it does:** Doc comments use `[SymbolName]` to create hyperlinks to other
identifiers. These render as clickable links on pkg.go.dev.
**Why:** Cross-references help users navigate the API. Links are concise and
don't clutter the plain-text rendering.
**Anti-pattern:** Using full URLs to godoc pages; not linking related types;
over-linking (every mention of every type).
**Code examples from source:**
```go
// net/http/server.go:65-70
// [Handler.ServeHTTP] should write reply headers and data to the [ResponseWriter]
// and then return. Returning signals that the request is finished; it
// is not valid to use the [ResponseWriter] or read from the
// [Request.Body] after or concurrently with the completion of the
// ServeHTTP call.
// os/file.go:9-11
// if a call that takes a file name fails, such as [Open] or [Stat], the error
// will include the failing file name when printed and will be of type
// [*PathError], which may be unpacked for more information.
```
---
## 5. Example Test Functions
**Pattern name:** `func ExampleXxx()` / `func ExampleType_Method()`
**Source citation:** [regexp/example_test.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/example_test.go#L13), [net/http/example_handle_test.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L16)
**What it does:** Functions named `Example`, `ExampleXxx`, or `ExampleType_Method`
in `_test.go` files serve as both executable tests and documentation. They include
an `// Output:` comment that `go test` verifies.
**Why:** Examples that compile, run, and are verified can never go stale. They appear
in `go doc` and pkg.go.dev alongside the relevant symbol. They teach by showing
real, working code.
**When to Use**
**Triggers:**
- You have a public function/type whose usage isn't obvious from the signature alone
- Your README examples have drifted from the actual API (broken examples in docs)
- You want examples that appear on pkg.go.dev AND are verified by `go test`
**Example — before:**
```go
// README.md (may be stale):
// ```go
// result := mylib.Process("input")
// fmt.Println(result.Data)
// ```
// ← compiles? who knows. API changed last month.
```
**Example — after:**
```go
// example_test.go
func ExampleProcess() {
result := mylib.Process("input")
fmt.Println(result.Data)
// Output:
// processed: input
}
// ← go test verifies this compiles and produces the expected output
```
**When NOT to Use**
**Don't write Example tests when:**
- The function signature is self-explanatory (`func Max(a, b int) int` doesn't need an example)
- The example would just restate the doc comment with no additional insight
- You're testing internal/unexported functions (examples must use the public API)
- The output is non-deterministic (timestamps, random values, goroutine ordering)
**Over-application example:**
```go
// Pointless — the signature tells you everything
func ExampleAbs() {
fmt.Println(math.Abs(-5))
// Output:
// 5
}
```
**Better alternative:**
```go
// Skip the example for trivial functions. Write examples for non-obvious behavior:
func ExampleAbs_nan() {
fmt.Println(math.Abs(math.NaN()))
// Output:
// NaN
}
// ← This teaches something surprising that the signature doesn't convey
```
**Why:** Examples exist to teach usage patterns that aren't obvious from the type signature and doc comment. Trivial examples add maintenance burden without teaching anything.
**Anti-pattern:** Examples that don't compile; examples without Output comments
(not verified); examples in README that drift from reality.
**Code examples from source:**
```go
// regexp/example_test.go:13-28
func Example() {
// Compile the expression once, usually at init time.
// Use raw strings to avoid having to quote the backslashes.
var validID = regexp.MustCompile(`^[a-z]+\[[0-9]+\]$`)
fmt.Println(validID.MatchString("adam[23]"))
fmt.Println(validID.MatchString("eve[7]"))
fmt.Println(validID.MatchString("Job[48]"))
fmt.Println(validID.MatchString("snakey"))
// Output:
// true
// true
// false
// false
}
```
```go
// net/http/example_handle_test.go:16-31
type countHandler struct {
mu sync.Mutex // guards n
n int
}
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
h.n++
fmt.Fprintf(w, "count is %d\n", h.n)
}
func ExampleHandle() {
http.Handle("/count", new(countHandler))
log.Fatal(http.ListenAndServe(":8080", nil))
}
```
---
## 6. Inline Code Examples in Doc Comments
**Pattern name:** Indented Code Blocks in Comments
**Source citation:** [os/file.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L16), [time/time.go#L928](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L928)
**What it does:** Doc comments include indented code snippets (4 spaces) that render
as preformatted code blocks in godoc.
**Why:** Shows typical usage patterns directly in the doc comment without requiring
a separate Example test function. Good for short, illustrative snippets.
**Anti-pattern:** Non-indented code that doesn't render as code; examples too long
for inline (use Example functions instead); examples that reference unexported symbols.
**Code examples from source:**
```go
// os/file.go:16-21
// Here is a simple example, opening a file and reading some of it.
//
// file, err := os.Open("file.go") // For read access.
// if err != nil {
// log.Fatal(err)
// }
// time/time.go:925-933
// To count the number of units in a [Duration], divide:
//
// second := time.Second
// fmt.Print(int64(second/time.Millisecond)) // prints 1000
//
// To convert an integer number of units to a Duration, multiply:
//
// seconds := 10
// fmt.Print(time.Duration(seconds)*time.Second) // prints 10s
```
---
## 7. Deprecated Annotations
**Pattern name:** `// Deprecated: ...` in Doc Comments
**Source citation:** [net/http/server.go#L57](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L57) (ErrWriteAfterFlush), [os/file.go#L93](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L93)
**What it does:** A paragraph starting with `Deprecated:` marks an identifier as
deprecated and explains what to use instead.
**Why:** Recognized by tooling (go vet, staticcheck, IDEs). Provides a migration
path without breaking backward compatibility.
**When to Use**
**Triggers:**
- You have a better replacement for an existing function but can't remove the old one (semver)
- Users are still calling a function that has known issues or a superior alternative
- You want IDEs to show a strikethrough and linters to warn on usage
**Example — before:**
```go
// Just delete it? Breaks everyone's code.
// Leave it silently? Users never learn about the better way.
```
**Example — after:**
```go
// ParseDuration parses a duration string.
//
// Deprecated: Use [time.ParseDuration] instead, which handles
// all standard duration formats.
func ParseDuration(s string) (time.Duration, error) {
return time.ParseDuration(s) // delegate to the replacement
}
```
**When NOT to Use**
**Don't deprecate when:**
- The function still works fine and there's no better replacement yet
- You're deprecating to "clean up" the API without a migration path (users will just ignore it)
- The replacement has a different behavior or isn't a drop-in substitute (document the difference instead)
- You're on v0.x and can just remove it (pre-1.0 allows breaking changes)
**Over-application example:**
```go
// Deprecated: Use NewClientV2 instead.
func NewClient(addr string) *Client { ... }
// But NewClientV2 has a completely different API:
func NewClientV2(cfg Config) (*ClientV2, error) { ... }
// Users can't just find-and-replace — this isn't a deprecation, it's a migration
```
**Better alternative:**
```go
// NewClient creates a client with default configuration.
// For advanced configuration (TLS, timeouts, connection pooling),
// use [NewClientWithConfig] instead.
func NewClient(addr string) *Client {
return NewClientWithConfig(Config{Addr: addr})
}
```
**Why:** Deprecation means "there's a better way to do the same thing." If the replacement requires a fundamentally different approach, provide a migration guide — don't just slap `Deprecated:` on it and leave users stranded.
**Anti-pattern:** Removing deprecated APIs (breaks semver); deprecating without
suggesting an alternative; using non-standard deprecation markers.
**Code example from source:**
```go
// net/http/server.go:59-62
// Deprecated: ErrWriteAfterFlush is no longer returned by
// anything in the net/http package. Callers should not
// compare errors against this variable.
ErrWriteAfterFlush = errors.New("unused")
```
---
## 8. Error Documentation Convention
**Pattern name:** "If there is an error, it will be of type [*XxxError]"
**Source citation:** [os/file.go#L388](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L388), 406
**What it does:** Functions document the concrete error type they return, enabling
callers to type-assert for additional context.
**Why:** Go's error handling relies on type assertions and `errors.Is/As`. Knowing
the concrete type lets callers extract structured information (path, operation,
underlying cause).
**Anti-pattern:** Returning opaque errors with no documented structure; returning
different error types from the same function without documenting which.
**Code example from source:**
```go
// os/file.go:388-390
// Open opens the named file for reading. If successful, methods on
// the returned file can be used for reading; the associated file
// descriptor has mode [O_RDONLY].
// If there is an error, it will be of type [*PathError].
func Open(name string) (*File, error) {
```
---
## 9. Concurrency Documentation
**Pattern name:** "Safe for concurrent use" / Concurrency Guarantees
**Source citation:** [net/http/transport.go#L79](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L79), [os/types.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/types.go#L17), [regexp/regexp.go#L77](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/regexp/regexp.go#L77)
**What it does:** Doc comments explicitly state the concurrency safety of a type
or note exceptions where concurrent use is not safe.
**Why:** Go programs are inherently concurrent. Without explicit documentation,
users must guess whether a type needs external synchronization.
**Anti-pattern:** Leaving concurrency safety undocumented; documenting it
inconsistently across methods; saying "thread-safe" (Java-ism, use "safe for
concurrent use by multiple goroutines").
**Code examples from source:**
```go
// net/http/transport.go:72-73
// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.
// os/types.go:17
// The methods of File are safe for concurrent use.
// regexp/regexp.go:77-79
// A Regexp is safe for concurrent use by multiple goroutines,
// except for configuration methods, such as [Regexp.Longest].
```
<!-- PATTERN_COMPLETE -->
+54 -265
View File
@@ -6,21 +6,21 @@ Patterns extracted from the Go standard library source code.
## 1. Sentinel Errors
### Source: [src/io/io.go#L40](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L40) (EOF), [src/errors/errors.go#L81](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L81) (ErrUnsupported)
### Source: `src/io/io.go:40-43` (EOF), `src/errors/errors.go:81-83` (ErrUnsupported)
```go
// [src/io/io.go#L40](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L40)
// 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#L47](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L47)
// src/io/io.go:47-49
var ErrUnexpectedEOF = errors.New("unexpected EOF")
```
```go
// [src/errors/errors.go#L81](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L81)
// src/errors/errors.go:81-83
var ErrUnsupported = New("unsupported operation")
```
@@ -36,86 +36,6 @@ if err == io.EOF {
**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).
### When to Use
**Triggers:**
- You have a specific, well-known failure condition callers need to check by identity
- Multiple packages compare against the same error value (`io.EOF`, `sql.ErrNoRows`)
- The error represents a **state** ("end of stream", "not found"), not a bug
**Example — before:**
```go
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ...")
var u User
err := row.Scan(&u.Name)
if err != nil {
return nil, fmt.Errorf("user not found") // caller can't distinguish "not found" from "db down"
}
return &u, nil
}
```
**Example — after:**
```go
var ErrUserNotFound = errors.New("users: not found")
func fetchUser(id int) (*User, error) {
row := db.QueryRow("SELECT ...")
var u User
err := row.Scan(&u.Name)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound // sentinel: callers can test with errors.Is
}
if err != nil {
return nil, fmt.Errorf("fetchUser: %w", err)
}
return &u, nil
}
```
### When NOT to Use
**Don't use this when:**
- The error condition is internal and no caller should branch on it — use `fmt.Errorf` instead
- You have too many sentinels (>5-6 per package) — consider a custom error type with a code field
- The error carries varying context (file path, user ID) — sentinels are for fixed conditions
**Over-application example:**
```go
// A sentinel for every possible failure — explosion of package-level vars
var (
ErrUserNotFound = errors.New("users: not found")
ErrUserInactive = errors.New("users: inactive")
ErrUserSuspended = errors.New("users: suspended")
ErrUserRateLimited = errors.New("users: rate limited")
ErrUserInvalidEmail = errors.New("users: invalid email")
ErrUserInvalidName = errors.New("users: invalid name")
ErrUserInvalidAge = errors.New("users: invalid age")
// 20 more...
)
```
**Better alternative:**
```go
// Use a typed error with a code when you have many distinct conditions
type UserError struct {
Code string // "not_found", "inactive", "suspended"
Field string // which field failed validation
Message string
}
func (e *UserError) Error() string { return "users: " + e.Message }
// Callers use errors.As to inspect
var uerr *UserError
if errors.As(err, &uerr) && uerr.Code == "not_found" { ... }
```
**Why:** Sentinels are for a small number of well-known states that callers frequently branch on. If you're creating dozens, you've outgrown the pattern — a structured error type with an enum/code field scales better and avoids polluting the package namespace.
### Anti-pattern
```go
@@ -132,15 +52,15 @@ func Read() error {
## 2. errors.New — Minimal Error Construction
### Source: [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
### Source: `src/errors/errors.go:62-69`
```go
// [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
// src/errors/errors.go:62-64
func New(text string) error {
return &errorString{text}
}
// [src/errors/errors.go#L66](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L66)
// src/errors/errors.go:66-69
type errorString struct {
s string
}
@@ -177,10 +97,10 @@ func doThing() error {
## 3. Error Wrapping with fmt.Errorf and %w
### Source: [src/fmt/errors.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L13), [src/fmt/errors.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L70)
### Source: `src/fmt/errors.go:13-23`, `src/fmt/errors.go:70-80`
```go
// [src/fmt/errors.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L13)
// src/fmt/errors.go:13-23
// Errorf formats according to a format specifier and returns the string
// as a value that satisfies error.
//
@@ -190,7 +110,7 @@ func doThing() error {
// Unwrap method returning a []error containing all the %w operands.
func Errorf(format string, a ...any) (err error) { ... }
// [src/fmt/errors.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L70)
// src/fmt/errors.go:70-80
type wrapError struct {
msg string
err error
@@ -217,85 +137,11 @@ return fmt.Errorf("open config: %w", err)
return fmt.Errorf("open config: %v", err)
```
### When to Use
**Triggers:**
- You're adding context to an error before returning it up the call stack
- The caller's error message would be meaningless without knowing *what* operation failed
- You have a chain of function calls and want a readable error trail: `"open config: read file: permission denied"`
**Example — before:**
```go
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err // caller sees "open /etc/app.conf: permission denied" — no context about WHO called ReadFile
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err // caller can't tell if this was a read error or a parse error
}
return &cfg, nil
}
```
**Example — after:**
```go
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config: %w", err) // wraps: callers can errors.Is(err, os.ErrNotExist)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("load config: parse %s: %v", path, err) // %v: hides internal JSON error type
}
return &cfg, nil
}
```
### 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.
### When NOT to Use
**Don't use this when:**
- You're wrapping at every single layer — the error message becomes `"a: b: c: d: e: permission denied"`
- The added context is obvious from the function name (the caller already knows what function they called)
- You're wrapping errors from a dependency you don't want in your API contract (use `%v` instead)
**Over-application example:**
```go
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
user, err := s.getUser(r)
if err != nil {
// "handleRequest: getUser: fetchFromDB: queryRow: scan: sql: no rows"
// The handler name adds nothing — the caller IS the handler
http.Error(w, fmt.Errorf("handleRequest: %w", err).Error(), 500)
return
}
}
```
**Better alternative:**
```go
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
user, err := s.getUser(r)
if err != nil {
// Log the full chain internally, return a clean message to the client
slog.Error("fetching user", "err", err, "path", r.URL.Path)
http.Error(w, "internal error", 500)
return
}
}
```
**Why:** Wrapping should add *meaningful* context that isn't already obvious. At HTTP handler boundaries, you typically log the error (with full chain) and return a generic response. Wrapping at every layer creates unreadable error strings and leaks internals.
### Anti-pattern
```go
@@ -310,10 +156,10 @@ return fmt.Errorf("internal: %w", internalErr) // now callers depend on interna
## 4. errors.Is — Checking Error Identity Through Chains
### Source: [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
### Source: `src/errors/wrap.go:30-44`
```go
// [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
// src/errors/wrap.go:30-44
func Is(err, target error) bool {
if err == nil || target == nil {
return err == target
@@ -377,10 +223,10 @@ if errors.Is(err, os.ErrNotExist) { ... } // works through wrapping
## 5. errors.As — Extracting Error Types Through Chains
### Source: [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
### Source: `src/errors/wrap.go:96-120`
```go
// [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
// src/errors/wrap.go:96-120
func As(err error, target any) bool {
if err == nil {
return false
@@ -406,7 +252,7 @@ if errors.As(err, &pathErr) {
### Go 1.24+: errors.AsType (generic version)
From [src/errors/errors.go#L48](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L48) doc:
From `src/errors/errors.go:48-56` doc:
```go
if perr, ok := errors.AsType[*fs.PathError](err); ok {
fmt.Println(perr.Path)
@@ -428,10 +274,10 @@ if errors.As(err, &pathErr) { ... } // works through wrapping
## 6. errors.Join — Multi-Error Aggregation
### Source: [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
### Source: `src/errors/join.go:20-39`
```go
// [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
// src/errors/join.go:20-39
func Join(errs ...error) error {
n := 0
for _, err := range errs {
@@ -454,11 +300,17 @@ func Join(errs ...error) error {
}
```
The `joinError` type implements `Unwrap() []error`, making both `Is` and `As` traverse correctly.
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.
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
@@ -467,77 +319,6 @@ errs = append(errs, closeCache())
return errors.Join(errs...) // nil if all nil
```
### When to Use
**Triggers:**
- You're closing/cleaning up multiple resources and each can fail independently
- A validation function checks multiple fields and you want ALL errors, not just the first
- You're running parallel operations and collecting errors from each
**Example — before:**
```go
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
if err := db.Close(); err != nil {
return err // stops here — cache and file leak!
}
if err := cache.Close(); err != nil {
return err // file still leaks
}
return file.Close()
}
```
**Example — after:**
```go
func cleanup(db *sql.DB, cache *redis.Client, file *os.File) error {
return errors.Join(
db.Close(),
cache.Close(),
file.Close(),
) // nil if all nil; contains all failures otherwise
}
```
### When NOT to Use
**Don't use this when:**
- Errors are causally related (error B was caused by error A) — use wrapping (`%w`) instead
- You're collecting errors across retries of the *same* operation — return the last error or wrap them causally
- The caller needs to distinguish which resource failed — Join loses that information
**Over-application example:**
```go
func fetchWithRetry(url string, attempts int) error {
var errs []error
for i := 0; i < attempts; i++ {
err := fetch(url)
if err == nil {
return nil
}
errs = append(errs, err) // collecting retry errors — misleading
}
return errors.Join(errs...) // caller sees 3 errors but they're the SAME operation
}
```
**Better alternative:**
```go
func fetchWithRetry(url string, attempts int) error {
var lastErr error
for i := 0; i < attempts; i++ {
lastErr = fetch(url)
if lastErr == nil {
return nil
}
}
return fmt.Errorf("fetch %s: %d attempts failed, last: %w", url, attempts, lastErr)
}
```
**Why:** `errors.Join` is for *independent* failures that all matter equally. For retries, the caller cares about the final failure and the retry count, not a list of (often identical) errors. For causal chains, wrapping preserves the relationship between cause and effect.
### Anti-pattern
```go
@@ -555,7 +336,7 @@ return lastErr
## 7. Custom Is() Method — Equivalence Classes
### Source: [src/errors/wrap.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L42) (doc comment), [src/context/context.go#L177](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L177)
### Source: `src/errors/wrap.go:42-44` (doc comment), `src/context/context.go:177-179`
From the `errors.Is` doc:
```go
@@ -569,7 +350,7 @@ From the `errors.Is` doc:
Real example from context:
```go
// [src/context/context.go#L177](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L177)
// src/context/context.go:177-179
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
@@ -594,10 +375,10 @@ func (e MyError) Is(target error) bool {
## 8. Error Wrapping in Custom Types (Unwrap pattern)
### Source: [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
### Source: `src/encoding/json/encode.go:276-293`
```go
// [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
// src/encoding/json/encode.go:276-282
type MarshalerError struct {
Type reflect.Type
Err error
@@ -650,12 +431,13 @@ func (e *MyError) Error() string { return e.Err.Error() }
## 9. ErrUnsupported — Feature Detection via Errors
### Source: [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
### Source: `src/errors/errors.go:76-83`
```go
// [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
// src/errors/errors.go:76-83
// ErrUnsupported indicates that a requested operation cannot be performed,
// because it is unsupported.
// 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
@@ -674,6 +456,8 @@ This pattern separates "what happened" (detailed context) from "what kind of fai
return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported)
```
Callers check the sentinel; the message provides context.
### Anti-pattern
```go
@@ -683,39 +467,46 @@ return errors.ErrUnsupported // no info about what operation or why
---
## 10. Error String Conventions
## 10. Error Value Patterns in net/http
### Source: [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
### Source: `src/net/http/server.go:39-56`
```go
// [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
// 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
```
- Lowercase (no capital first letter)
- No trailing punctuation
- Package prefix for disambiguation
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")
errors.New("Connection has been hijacked") // Go convention: lowercase
// 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
// DON'T: End error strings with punctuation
errors.New("connection failed.") // no trailing period
```
---
@@ -742,5 +533,3 @@ Is this a specific, well-known condition?
| Aggregate multiple errors | `errors.Join(err1, err2)` |
| Make custom types traversable | Implement `Unwrap() error` |
| Define error equivalence | Implement `Is(error) bool` |
<!-- PATTERN_COMPLETE -->
+28 -318
View File
@@ -6,22 +6,22 @@ Patterns extracted from the Go standard library source code.
## 1. Small Interfaces (1-2 Methods)
### Source: [src/io/io.go#L80](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L80) (Reader), `93-103` (Writer), `105-109` (Closer)
### 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#L80](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L80)
// src/io/io.go:80-92
type Reader interface {
Read(p []byte) (n int, err error)
}
// [src/io/io.go#L93](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L93)
// src/io/io.go:93-103
type Writer interface {
Write(p []byte) (n int, err error)
}
// [src/io/io.go#L105](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L105)
// src/io/io.go:105-109
type Closer interface {
Close() error
}
@@ -31,74 +31,6 @@ type Closer interface {
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`.
### When to Use
**Triggers:**
- You're defining a function that only needs ONE capability from its argument (reading, writing, closing)
- You want maximum reusability — many different types should be able to satisfy your requirement
- You're tempted to create a big interface but realize most consumers only use 1-2 methods
**Example — before:**
```go
// Accepts only *os.File — can't use with buffers, HTTP bodies, test mocks
func countLines(f *os.File) (int, error) {
scanner := bufio.NewScanner(f)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
**Example — after:**
```go
// Accepts io.Reader — works with files, HTTP bodies, strings.NewReader, gzip.Reader, etc.
func countLines(r io.Reader) (int, error) {
scanner := bufio.NewScanner(r)
count := 0
for scanner.Scan() { count++ }
return count, scanner.Err()
}
```
### When NOT to Use
**Don't use this when:**
- You only have one implementation and no tests — you're adding indirection for no reason
- The function genuinely needs multiple capabilities together (reading + seeking + closing)
- You're creating an interface to match a single concrete type that you control
**Over-application example:**
```go
// Interface with one implementation, no tests, no external consumers
type Configurer interface {
LoadConfig(path string) (*Config, error)
}
type fileConfigurer struct{}
func (f *fileConfigurer) LoadConfig(path string) (*Config, error) {
return parseFile(path)
}
func NewApp(c Configurer) *App {
// c is always *fileConfigurer — the interface adds nothing
return &App{cfg: c}
}
```
**Better alternative:**
```go
// Just use the concrete type until you actually need the abstraction
func NewApp(cfgPath string) *App {
cfg := parseFile(cfgPath)
return &App{cfg: cfg}
}
```
**Why:** Interfaces in Go should be discovered through usage, not predicted. "Accept interfaces" means accept them at the *boundaries* where multiple types actually flow through. If you have one implementation and no tests that need a mock, you have a premature abstraction.
### Anti-pattern
```go
@@ -120,30 +52,30 @@ Large interfaces are hard to implement, hard to mock, and couple consumers to ca
## 2. Interface Composition
### Source: [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
### Source: `src/io/io.go:131-155`
Compose small interfaces into larger ones only when needed:
```go
// [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
// src/io/io.go:131-134
type ReadWriter interface {
Reader
Writer
}
// [src/io/io.go#L136](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L136)
// src/io/io.go:136-139
type ReadCloser interface {
Reader
Closer
}
// [src/io/io.go#L141](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L141)
// src/io/io.go:141-144
type WriteCloser interface {
Writer
Closer
}
// [src/io/io.go#L146](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L146)
// src/io/io.go:146-150
type ReadWriteCloser interface {
Reader
Writer
@@ -168,13 +100,13 @@ func processData(rw ReadWriteCloser) {
## 3. Accept Interfaces, Return Structs
### Source: [src/io/io.go#L461](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L461) (LimitReader), [src/io/io.go#L618](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L618) (TeeReader)
### 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#L467](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L467)
// src/io/io.go:467-471
type LimitedReader struct {
R Reader // underlying reader
N int64 // max bytes remaining
@@ -182,7 +114,7 @@ type LimitedReader struct {
```
```go
// [src/io/io.go#L618](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L618)
// src/io/io.go:618-620
func TeeReader(r Reader, w Writer) Reader {
return &teeReader{r, w}
}
@@ -195,69 +127,6 @@ func TeeReader(r Reader, w Writer) Reader {
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).
### When to Use
**Triggers:**
- You're writing a function/constructor that operates on a capability (reading, hashing, connecting)
- Your return type has useful fields or methods beyond the interface contract
- You want callers to pass anything that satisfies the contract, but return something concrete they can inspect
**Example — before:**
```go
// Too restrictive input, too vague output
func NewLogger(f *os.File) io.Writer {
return &logger{out: f, level: "info"} // hides SetLevel, Flush methods
}
```
**Example — after:**
```go
type Logger struct {
out io.Writer
level string
}
func (l *Logger) SetLevel(lvl string) { l.level = lvl }
func (l *Logger) Flush() error { /* ... */ }
// Accept interface (any io.Writer), return struct (full access)
func NewLogger(w io.Writer) *Logger {
return &Logger{out: w, level: "info"}
}
```
### When NOT to Use
**Don't use this when:**
- Your function is internal and only ever called with one concrete type
- Returning an interface is genuinely better because the concrete type is an implementation detail that may change
- The struct's exported fields would expose dangerous internals
**Over-application example:**
```go
// Accepting an interface when only one concrete type makes sense
func NewDatabaseMigrator(db interface {
Exec(query string, args ...any) (sql.Result, error)
Query(query string, args ...any) (*sql.Rows, error)
Begin() (*sql.Tx, error)
}) *Migrator {
// This custom interface exactly matches *sql.DB — just accept *sql.DB
return &Migrator{db: db}
}
```
**Better alternative:**
```go
// Accept the concrete type when the abstraction doesn't buy anything
func NewDatabaseMigrator(db *sql.DB) *Migrator {
return &Migrator{db: db}
}
```
**Why:** "Accept interfaces" doesn't mean "always accept interfaces." If you define a bespoke interface that matches exactly one concrete type and no one else will implement it, you've just added indirection. The guideline targets *standard* interfaces (io.Reader, io.Writer) that many types satisfy.
### Anti-pattern
```go
@@ -272,7 +141,7 @@ func NewServer() ServerInterface // hides useful config fields
## 4. Interface Satisfaction as a Compile-Time Check
### Source: [src/io/io.go#L645](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L645), [src/net/http/server.go#L4071](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4071)
### Source: `src/io/io.go:645`, `src/net/http/server.go:4071`
```go
// src/io/io.go:645
@@ -299,10 +168,10 @@ func doSomething(w ResponseWriter) {
## 5. Interface-Based Polymorphism (sort.Interface)
### Source: [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
### Source: `src/sort/sort.go:16-41`
```go
// [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
// src/sort/sort.go:16-41
type Interface interface {
Len() int
Less(i, j int) bool
@@ -328,17 +197,17 @@ Note: Since Go 1.21, `slices.SortFunc` is preferred for slices (generic + faster
## 6. The Adapter Pattern (HandlerFunc)
### Source: [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
### Source: `src/net/http/server.go:2334-2342`
```go
// [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
// 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#L2341](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2341)
// src/net/http/server.go:2341-2342
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
@@ -349,78 +218,6 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
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.
### When to Use
**Triggers:**
- You have an interface with a single method and users frequently implement it with a bare function
- You want to accept both struct-based and function-based implementations of the same behavior
- Requiring a struct definition for simple cases feels like boilerplate
**Example — before:**
```go
type Processor interface {
Process(data []byte) error
}
// User must create a whole struct just to use a function
type upperProcessor struct{}
func (u upperProcessor) Process(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}
```
**Example — after:**
```go
type Processor interface {
Process(data []byte) error
}
// Adapter: any function with the right signature becomes a Processor
type ProcessorFunc func([]byte) error
func (f ProcessorFunc) Process(data []byte) error { return f(data) }
// Now users can write:
pipeline.Use(ProcessorFunc(func(data []byte) error {
fmt.Println(strings.ToUpper(string(data)))
return nil
}))
```
### When NOT to Use
**Don't use this when:**
- The interface has more than one method — adapters only work for single-method interfaces
- Implementations typically need state (struct fields) that closures would awkwardly close over
- The function signature is complex enough that a named type with methods is clearer
**Over-application example:**
```go
// Adapter for a multi-method interface — doesn't work
type StorageFunc func(key string, data []byte) error
func (f StorageFunc) Store(key string, data []byte) error { return f(key, data) }
func (f StorageFunc) Load(key string) ([]byte, error) { /* can't implement! */ }
func (f StorageFunc) Delete(key string) error { /* can't implement! */ }
```
**Better alternative:**
```go
// For multi-method interfaces, use a struct (or split the interface)
type MemoryStorage struct {
data map[string][]byte
}
func (m *MemoryStorage) Store(key string, data []byte) error { ... }
func (m *MemoryStorage) Load(key string) ([]byte, error) { ... }
func (m *MemoryStorage) Delete(key string) error { ... }
```
**Why:** The adapter pattern bridges functions to interfaces. Functions have one signature, so adapters only work for single-method interfaces. If your interface has multiple methods, callers need a struct anyway — the adapter just adds confusion.
### Anti-pattern
```go
@@ -436,15 +233,15 @@ func (h myHandler) ServeHTTP(w ResponseWriter, r *Request) {
## 7. Optional Interfaces (Runtime Feature Detection)
### Source: [src/net/http/server.go#L165](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L165) (Flusher), [src/net/http/server.go#L183](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L183) (Hijacker)
### Source: `src/net/http/server.go:165-175` (Flusher), `src/net/http/server.go:183-206` (Hijacker)
```go
// [src/net/http/server.go#L165](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L165)
// src/net/http/server.go:165-170
type Flusher interface {
Flush()
}
// [src/net/http/server.go#L183](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L183)
// src/net/http/server.go:183-206
type Hijacker interface {
Hijack() (net.Conn, *bufio.ReadWriter, error)
}
@@ -462,91 +259,6 @@ if flusher, ok := w.(Flusher); ok {
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.
### When to Use
**Triggers:**
- Some implementations support a capability but others don't (flushing, hijacking, seeking)
- You want to keep the core interface small but allow optimizations when available
- You're writing middleware that should enhance behavior when possible, not require it
**Example — before:**
```go
// Forces ALL stores to implement caching, even simple ones
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
InvalidateCache() error // not all stores have a cache!
}
```
**Example — after:**
```go
type Store interface {
Get(key string) ([]byte, error)
Set(key string, val []byte) error
}
// Optional capability — check at runtime
type Cacheable interface {
InvalidateCache() error
}
func refreshAll(s Store) {
if c, ok := s.(Cacheable); ok {
c.InvalidateCache() // only if supported
}
}
```
### When NOT to Use
**Don't use this when:**
- All implementations will always support the capability — just put it in the main interface
- The capability is required for correctness, not just optimization
- You have only 2-3 implementations and a simple interface split handles it better
**Over-application example:**
```go
// Every HTTP handler MUST write a response — this isn't optional
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
// Don't make response-writing "optional"
type ResponseWriter interface {
Header() Header
}
type BodyWriter interface {
Write([]byte) (int, error) // NOT optional — every response needs a body mechanism
}
func handle(w ResponseWriter) {
if bw, ok := w.(BodyWriter); ok { // wrong: Write is fundamental, not optional
bw.Write([]byte("hello"))
}
}
```
**Better alternative:**
```go
// Write is fundamental — keep it in the core interface
type ResponseWriter interface {
Header() Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
// Only truly optional capabilities get separate interfaces
type Flusher interface {
Flush()
}
```
**Why:** Optional interfaces are for progressive enhancement — capabilities that some implementations support but others legitimately don't. If every implementation must support it for the system to work, it belongs in the core interface. Overusing type assertions makes code fragile and harder to reason about.
### Anti-pattern
```go
@@ -564,10 +276,10 @@ type ResponseWriter interface {
## 8. The Stringer Interface (Convention-Based Behavior)
### Source: [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
### Source: `src/fmt/print.go:63-66`
```go
// [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
// src/fmt/print.go:63-66
type Stringer interface {
String() string
}
@@ -596,10 +308,10 @@ func printThing(v any) string {
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
### Source: [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
### Source: `src/io/io.go:410-417`
```go
// [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
// 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.
@@ -632,15 +344,15 @@ func Copy(dst Writer, src Reader) {
## 10. The driver.Driver Pattern (Plugin Interfaces)
### Source: [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85), `104-112`
### Source: `src/database/sql/driver/driver.go:85-97`, `104-112`
```go
// [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85)
// src/database/sql/driver/driver.go:85-97
type Driver interface {
Open(name string) (Conn, error)
}
// [src/database/sql/driver/driver.go#L104](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L104)
// src/database/sql/driver/driver.go:104-112
type DriverContext interface {
OpenConnector(name string) (Connector, error)
}
@@ -676,5 +388,3 @@ type Driver interface {
| 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` |
<!-- PATTERN_COMPLETE -->
+179 -277
View File
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
## 1. Package-Level Documentation
### Source: [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5), [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5), [src/context/context.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L5)
### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57`
```go
// [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5)
// 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
@@ -22,7 +22,7 @@ package io
```
```go
// [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5)
// 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
@@ -43,6 +43,7 @@ The package comment:
### 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
@@ -54,6 +55,10 @@ 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
```
---
@@ -69,10 +74,16 @@ package http
- `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, lowercase, no underscores or mixedCaps**. The package name is part of every qualified identifier:
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
@@ -84,14 +95,18 @@ context.Context // the type IS the context
### Anti-pattern
```go
// DON'T: Stutter
// 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: Utility package names
// 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
```
---
@@ -104,97 +119,64 @@ package common // everything ends up here
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`. This lets you share code between sub-packages without making it public API.
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
- `net/http/internal/ascii` → importable by `net/http` and children
- NOT importable by `net/url` or any other package
This lets you share code between sub-packages without making it part of the public API.
### When to Use
### Usage Guidelines
**Triggers:**
- You have helper code shared between sub-packages but NOT part of your public API
- You're tempted to export a function "just for testing" — put it in `internal/` instead
- Your package has grown and you want to split it without committing to new public APIs
**Example — before:**
```go
// pkg/mylib/helpers.go — exported just so pkg/mylib/sub can use it
package mylib
func ParseInternalFormat(s string) Thing { ... } // now anyone can depend on this!
```
**Example — after:**
```go
// pkg/mylib/internal/parse/parse.go
package parse
func InternalFormat(s string) Thing { ... } // only importable by pkg/mylib and children
// pkg/mylib/sub/handler.go
import "pkg/mylib/internal/parse" // ✓ allowed
myproject/
├── internal/ # shared across the project, but not importable externally
│ ├── auth/
│ └── metrics/
├── cmd/
│ └── server/
└── pkg/ # actually public API (if you use this convention)
└── client/
```
### When NOT to Use
**Don't use `internal/` when:**
- The code is only used by a single package (just keep it unexported in that package)
- You're hiding code that *should* be public API — `internal/` isn't a staging area for "maybe later"
- You have a flat package structure with no sub-packages (no one to share with)
**Over-application example:**
```go
// pkg/mylib/internal/config/config.go
package config
// Only used by pkg/mylib itself — no sub-packages import this
func DefaultTimeout() time.Duration { return 30 * time.Second }
```
**Better alternative:**
```go
// pkg/mylib/config.go — just make it unexported in the parent package
package mylib
func defaultTimeout() time.Duration { return 30 * time.Second }
```
**Why:** `internal/` adds directory structure complexity. If you have no sub-packages sharing the code, an unexported function in the parent package is simpler and achieves the same encapsulation.
### Anti-pattern
```go
// DON'T: Export implementation details
// DON'T: Export implementation details that should be internal
package mylib
func HelperThatOnlyIUse() {} // pollutes API surface
// DO: Move to internal/
// 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: `src/io/io.go` — exported vs unexported
### 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}
}
@@ -202,27 +184,42 @@ func TeeReader(r Reader, w Writer) Reader { // exported constructor
### Why
`teeReader` is unexported because:
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` (interface) — maximum flexibility
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?
buffer []byte // internal state
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#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/http2.go#L37)
### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31`
```go
// src/net/http/http2.go:37
@@ -233,86 +230,18 @@ func init() {
### Why
The stdlib uses `init()` for:
`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. Should have no side effects beyond registration
2. No errors possible (can't return error from init)
3. Keep them short
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
### When to Use
**Triggers:**
- You're writing a driver or plugin that needs to register itself with a central registry on import
- The registration is side-effect-only (no return value, can't fail)
- You want `import _ "mydb/driver"` to make the driver available without explicit setup
**Example — before:**
```go
// main.go — user must manually register every driver
func main() {
postgres.Register() // easy to forget
mysql.Register() // order matters?
sqlite.Register()
}
```
**Example — after:**
```go
// postgres/driver.go
func init() {
sql.Register("postgres", &Driver{}) // auto-registers on import
}
// main.go — import for side-effect
import _ "github.com/lib/pq" // driver registers itself
```
### When NOT to Use
**Don't use `init()` when:**
- The initialization can fail (you can't return errors from `init()`)
- The setup requires configuration or parameters (init takes no args)
- You need to control initialization order across packages
- It's a one-off application (not a library/driver) — just call setup in `main()`
**Over-application example:**
```go
// internal/metrics/metrics.go
func init() {
// Bad: init() hides this dependency, makes testing impossible,
// and panics if prometheus isn't reachable
prometheus.MustRegister(requestCounter)
prometheus.MustRegister(errorCounter)
prometheus.MustRegister(latencyHistogram)
}
```
**Better alternative:**
```go
// internal/metrics/metrics.go
func Register(reg prometheus.Registerer) error {
if err := reg.Register(requestCounter); err != nil {
return fmt.Errorf("registering request counter: %w", err)
}
// ...
return nil
}
// main.go
func main() {
if err := metrics.Register(prometheus.DefaultRegisterer); err != nil {
log.Fatal(err)
}
}
```
**Why:** `init()` is invisible, untestable, and can't fail gracefully. Use it only when the registration pattern demands it (database/sql drivers, codec registration) and failure is impossible.
### Anti-pattern
```go
@@ -322,11 +251,16 @@ func init() {
cache = loadGigabyteFile() // blocks startup
}
// DO: Prefer explicit setup in main()
// 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)
log.Fatal(err) // clear failure point
}
}
```
@@ -335,10 +269,12 @@ func main() {
## 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:
### 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 (idiom from Rob Pike/Dave Cheney):
// The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney):
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
@@ -347,6 +283,12 @@ func WithTimeout(d time.Duration) Option {
}
}
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 {
@@ -356,10 +298,10 @@ func NewServer(addr string, opts ...Option) *Server {
}
```
### What stdlib uses: Config structs
### What the stdlib uses instead: Config structs
```go
// net/http — struct literal configuration
// src/net/http/server.go (Server struct acts as config)
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
@@ -372,27 +314,40 @@ srv := &http.Server{
| Approach | When |
|----------|------|
| Config struct | Few options, all data (stdlib preference) |
| Functional options | Many options, some involve behavior, public API stability |
| 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#L2638](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2638), [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836`
```go
// src/net/http/server.go:2638
// src/net/http/server.go:2639
func NewServeMux() *ServeMux {
return &ServeMux{}
return new(ServeMux)
}
// [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
// 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)
@@ -402,19 +357,24 @@ func OpenDB(c driver.Connector) *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`)
- `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
func NewWriter() io.Writer { return &myWriter{} } // hides methods
// DON'T: Constructor that returns interface (hides useful methods)
func NewWriter() io.Writer { return &myWriter{} }
// DON'T: Require constructor when zero value works
// var b bytes.Buffer ← just works
type Buffer struct {
buf []byte
// ...
}
// var b bytes.Buffer ← just works, no New needed
```
---
@@ -430,42 +390,54 @@ src/
├── net/ # network primitives
│ ├── http/ # HTTP protocol
│ └── url/ # URL parsing
├── encoding/
├── encoding/ # encoding interfaces
│ ├── json/ # JSON codec
│ └── xml/ # XML codec
├── database/
│ └── sql/ # SQL abstraction
│ └── driver/ # SPI for drivers
│ └── sql/ # SQL database abstraction
│ └── driver/ # SPI for database drivers
└── context/ # cancellation propagation
```
### Why
Each package has a single, clear responsibility. Packages communicate through interfaces, not shared state.
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 (50 packages with 1 file each)
package user
package order
package payment
// 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 Layering — User vs Implementor (database/sql)
## 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):**
@@ -482,19 +454,41 @@ type Conn interface {
### 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.
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#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132), [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252`
```go
// [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132) (from doc)
// 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 {
@@ -508,7 +502,7 @@ The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic.
```
```go
// [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
// src/net/http/server.go:244-252
var (
ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"}
@@ -521,129 +515,37 @@ type contextKey struct {
### Why
- **Unexported key type** prevents other packages from accessing your values
- **Type-safe accessors** avoid repeated type assertions
- **Pointer-based keys** guarantee uniqueness
### When to Use
**Triggers:**
- You need to pass request-scoped metadata through a call chain (user ID, trace ID, auth token)
- The data crosses package boundaries and isn't appropriate as a function parameter
- You want type safety — only your package should read/write its context values
**Example — before:**
```go
// String keys — any package can collide or access your values
ctx = context.WithValue(ctx, "userID", 42)
uid := ctx.Value("userID").(int) // panics if wrong type or missing
```
**Example — after:**
```go
type ctxKey struct{}
func WithUserID(ctx context.Context, id int) context.Context {
return context.WithValue(ctx, ctxKey{}, id)
}
func UserID(ctx context.Context) (int, bool) {
id, ok := ctx.Value(ctxKey{}).(int)
return id, ok
}
```
### When NOT to Use
**Don't use context values when:**
- The data is a required function parameter (pass it explicitly)
- The data controls behavior/logic (timeouts, retry counts) — use function args or config structs
- You're using it to avoid refactoring function signatures
- The value is large or expensive to retrieve (context isn't a cache)
**Over-application example:**
```go
// Passing database connection through context — it's required everywhere!
func HandleRequest(ctx context.Context) {
db := DatabaseFromContext(ctx) // nil if forgotten — runtime panic
users, err := db.Query(ctx, "SELECT ...")
}
```
**Better alternative:**
```go
// Make the dependency explicit
type Handler struct {
db *sql.DB
}
func (h *Handler) HandleRequest(ctx context.Context) {
users, err := h.db.QueryContext(ctx, "SELECT ...")
}
```
**Why:** Context values are untyped, invisible in function signatures, and can silently be nil. They're meant for *request-scoped metadata* that crosses API boundaries (trace IDs, auth tokens), not for dependency injection or configuration.
- **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 (collision risk)
// 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!
```
---
## 11. Struct Tags for Codec Configuration
### Source: [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17), [src/encoding/json/encode.go#L101](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L101)
```go
// [src/encoding/json/tags.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/tags.go#L17)
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 |
| 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 |
| API layers | Separate user from implementor (SPI) |
| 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 or functional options |
<!-- PATTERN_COMPLETE -->
| Configuration | Struct literals (stdlib) or functional options (community) |
-601
View File
@@ -1,601 +0,0 @@
# Struct Design Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Zero-Value Usability
**Pattern name:** Zero Value Ready
**Source citation:** [net/http/client.go#L31](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L31), [strings/builder.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L14)
**What it does:** Structs are designed so their zero value is immediately useful without
explicit initialization. Nil fields fall back to sensible defaults at method call time.
**Why:** Eliminates mandatory constructors, reduces boilerplate, makes the type
self-documenting about its defaults. Users can write `var c http.Client` and start
making requests.
**When to Use**
**Triggers:**
- You're designing a type where the "empty" or "default" state is meaningful and safe
- Users should be able to write `var x MyType` and immediately call methods
- Your struct's nil/zero fields can fall back to sensible defaults at call time
**Example — before:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration
}
// Panics on zero value — store is nil!
func (c *Cache) Set(k string, v []byte) { c.store[k] = v }
```
**Example — after:**
```go
type Cache struct {
store map[string][]byte
ttl time.Duration // zero means no expiry
}
func (c *Cache) Set(k string, v []byte) {
if c.store == nil {
c.store = make(map[string][]byte) // lazy init on first use
}
c.store[k] = v
}
```
### When NOT to Use
**Don't use this when:**
- The type has mandatory dependencies that *must* be provided (a DB connection, an io.Reader with no sensible default)
- The zero value would be dangerous rather than merely useless (e.g., a zero-value security config that disables auth)
- Lazy initialization adds per-call overhead on a hot path and the constructor is called once
**Over-application example:**
```go
// Trying to make a DB-backed store zero-value ready
type UserStore struct {
db *sql.DB // nil means... what? No sensible default exists.
}
func (s *UserStore) Get(id int) (*User, error) {
if s.db == nil {
return nil, errors.New("no database configured") // every call pays for nil check
}
// ...
}
```
**Better alternative:**
```go
// A constructor makes the requirement explicit
func NewUserStore(db *sql.DB) *UserStore {
return &UserStore{db: db}
}
```
**Why:** When there's no meaningful default for a dependency, forcing zero-value usability
just moves the error from compile time (missing argument) to runtime (nil check on every call).
Use a constructor instead.
**Anti-pattern:** Requiring a constructor for basic use; panicking on zero-value use;
requiring all fields be set before the type is functional.
**Code examples from source:**
```go
// net/http/client.go:30-34
// A Client is an HTTP client. Its zero value ([DefaultClient]) is a
// usable client that uses [DefaultTransport].
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
// net/http/client.go:109
var DefaultClient = &Client{}
```
```go
// strings/builder.go:14-16
// A Builder is used to efficiently build a string using [Builder.Write] methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
addr *Builder
buf []byte
}
```
```go
// bytes/buffer.go:19-20
// A Buffer is a variable-sized buffer of bytes with [Buffer.Read] and [Buffer.Write] methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
buf []byte
off int
lastRead readOp
}
```
---
## 2. Unexported Struct with Exported Wrapper
**Pattern name:** Indirection via Unexported Impl
**Source citation:** [os/types.go#L15](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/types.go#L15), [os/file_unix.go#L59](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file_unix.go#L59)
**What it does:** The exported type (`File`) embeds a pointer to an unexported type
(`*file`) that holds the real implementation state. Users interact only with the
exported wrapper.
**Why:** Prevents users from directly constructing or copying the implementation struct.
Allows platform-specific implementations behind a uniform exported API. The extra
indirection ensures finalizers close the correct descriptor.
**Anti-pattern:** Exporting all implementation fields; allowing users to construct
the struct via a literal (bypassing invariants); needing platform #ifdefs in the
public API.
**Code example from source:**
```go
// os/types.go:15-20
// File represents an open file descriptor.
//
// The methods of File are safe for concurrent use.
type File struct {
*file // os specific
}
// os/file_unix.go:59-71
// file is the real representation of *File.
// The extra level of indirection ensures that no clients of os
// can overwrite this data, which could cause the finalizer
// to close the wrong file descriptor.
type file struct {
pfd poll.FD
name string
dirinfo atomic.Pointer[dirInfo]
nonblock bool
stdoutOrErr bool
appendMode bool
inRoot bool
}
```
---
## 3. Constructor Functions (NewXxx)
**Pattern name:** NewXxx Constructor
**Source citation:** [bufio/scan.go#L89](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L89), [bufio/bufio.go#L50](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/bufio.go#L50)
**What it does:** A package-level function `NewXxx(deps) *Xxx` constructs the type
with required dependencies and internal defaults that can't be expressed via zero
value alone.
**Why:** When a type has mandatory dependencies (e.g., an `io.Reader`), a constructor
clearly communicates what's required. The constructor can set internal invariants
(buffer sizes, split functions) that users shouldn't need to know about.
**When to Use**
**Triggers:**
- Your type has mandatory dependencies that can't be expressed as zero values (an `io.Reader`, a DB connection)
- Internal invariants must be set up (buffer allocation, goroutine start)
- The type isn't useful without initialization (unlike `sync.Mutex` or `bytes.Buffer`)
**Example — before:**
```go
type Parser struct {
lexer *Lexer
buf []Token
maxDepth int
}
// User must know about all internal state:
p := &Parser{lexer: NewLexer(input), buf: make([]Token, 0, 64), maxDepth: 100}
```
**Example — after:**
```go
func NewParser(input io.Reader) *Parser {
return &Parser{
lexer: NewLexer(input),
buf: make([]Token, 0, 64),
maxDepth: 100,
}
}
// User writes:
p := NewParser(file)
```
### When NOT to Use
**Don't use this when:**
- The type's zero value is already useful — adding a constructor creates unnecessary ceremony
- Your constructor takes 5+ optional parameters (use a config struct instead)
- The "mandatory dependency" is actually optional and has a sensible default (e.g., a logger defaulting to `slog.Default()`)
**Over-application example:**
```go
// Constructor for something that doesn't need one
func NewCounter() *Counter {
return &Counter{count: 0} // zero value already does this!
}
// Forces users to write:
c := NewCounter()
```
**Better alternative:**
```go
type Counter struct {
count int64
}
// Users write: var c Counter — done.
```
**Why:** If the zero value works, a constructor is just noise. It obscures the type's
actual simplicity and makes users wonder what hidden initialization they're missing.
**Anti-pattern:** Forcing users to manually set unexported fields; having a constructor
that takes 10 optional parameters (use config struct instead); requiring New when
zero value would suffice.
**Code examples from source:**
```go
// bufio/scan.go:89-96
func NewScanner(r io.Reader) *Scanner {
return &Scanner{
r: r,
split: ScanLines,
maxTokenSize: MaxScanTokenSize,
}
}
```
```go
// bufio/bufio.go:50-62
func NewReaderSize(rd io.Reader, size int) *Reader {
// Is it already a Reader?
b, ok := rd.(*Reader)
if ok && len(b.buf) >= size {
return b
}
r := new(Reader)
r.reset(make([]byte, max(size, minReadBufferSize)), rd)
return r
}
// NewReader returns a new [Reader] whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}
```
```go
// net/http/request.go:867-869
func NewRequest(method, url string, body io.Reader) (*Request, error) {
return NewRequestWithContext(context.Background(), method, url, body)
}
```
---
## 4. NewXxx with Size/Options Variant
**Pattern name:** NewXxx / NewXxxSize Pair
**Source citation:** [bufio/bufio.go#L50](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/bufio.go#L50), 62, 589, 607
**What it does:** Provides two constructors — one with defaults (`NewReader`) and one
with explicit configuration (`NewReaderSize`). The default version calls the
configurable one.
**Why:** Most users want the default; power users need control. Layering avoids a
proliferation of constructor parameters for the common case.
**Anti-pattern:** Having only the complex constructor; making users guess the right
buffer size; inconsistent naming (e.g., `NewReaderWithSize`).
**Code example from source:**
```go
// bufio/bufio.go:589-607
func NewWriterSize(w io.Writer, size int) *Writer {
// ...
}
func NewWriter(w io.Writer) *Writer {
return NewWriterSize(w, defaultBufSize)
}
```
---
## 5. Config Struct Pattern
**Pattern name:** Configuration Struct (Exported Fields, Nil-Means-Default)
**Source citation:** [net/http/server.go#L3020](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3020), [crypto/tls/common.go#L566](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/tls/common.go#L566)+, [log/slog/handler.go#L135](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/log/slog/handler.go#L135)
**What it does:** A struct with exported, documented fields provides all
configuration knobs. Nil/zero values always mean "use the default".
**Why:** Self-documenting via godoc; no need for a setter method per option; easy to
construct partially; serializable; the zero value works. This is Go's primary
configuration pattern (preferred over functional options in the stdlib).
**When to Use**
**Triggers:**
- Your constructor has 4+ optional parameters that would make a function signature unwieldy
- You want users to see all options in one place with godoc documentation
- Zero/nil values should mean "use the default" — no required fields beyond what the constructor demands
**Example — before:**
```go
// 7 parameters — impossible to remember the order
func NewServer(addr string, handler http.Handler, readTimeout, writeTimeout time.Duration,
maxConns int, logger *log.Logger, tlsConfig *tls.Config) *Server { ... }
```
**Example — after:**
```go
type ServerConfig struct {
Addr string // ":8080" if empty
Handler http.Handler // http.DefaultServeMux if nil
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxConns int // 1000 if zero
Logger *log.Logger // log.Default() if nil
TLSConfig *tls.Config // plain HTTP if nil
}
func NewServer(cfg ServerConfig) *Server { ... }
```
### When NOT to Use
**Don't use this when:**
- You only have 1–2 options — just use function parameters
- The options are truly required (not optional) — they belong in the constructor signature
- Your config struct has methods or behavior — it's no longer a plain config, it's becoming a type
**Over-application example:**
```go
// Config struct for two required parameters
type ClientConfig struct {
BaseURL string // required!
APIKey string // required!
}
func NewClient(cfg ClientConfig) (*Client, error) {
if cfg.BaseURL == "" { return nil, errors.New("base URL required") }
if cfg.APIKey == "" { return nil, errors.New("API key required") }
// ...
}
```
**Better alternative:**
```go
// Required params are function args; only truly optional things go in a config
func NewClient(baseURL, apiKey string, opts *ClientOptions) (*Client, error) {
// baseURL and apiKey are enforced by the signature
// opts can be nil for defaults
}
```
**Why:** Config structs shine for *optional* configuration. When fields are required,
the compiler can't enforce them — you end up validating at runtime what the type system
could have caught. Keep required parameters as explicit function arguments.
**Anti-pattern:** Undocumented fields; requiring all fields set; using sentinel values
other than zero/nil for defaults; providing setters when direct assignment works.
**Code example from source:**
```go
// net/http/server.go:3020-3075 (abbreviated)
type Server struct {
Addr string // ":http" if empty
Handler Handler // http.DefaultServeMux if nil
TLSConfig *tls.Config // optional
ReadTimeout time.Duration // zero means no timeout
WriteTimeout time.Duration // zero means no timeout
MaxHeaderBytes int // DefaultMaxHeaderBytes if zero
ErrorLog *log.Logger // log.Default() if nil
// ...
}
```
```go
// log/slog/handler.go:135-175
type HandlerOptions struct {
AddSource bool
Level Leveler // LevelInfo if nil
ReplaceAttr func(groups []string, a Attr) Attr
}
// Usage: If opts is nil, the default options are used.
func NewTextHandler(w io.Writer, opts *HandlerOptions) *TextHandler {
if opts == nil {
opts = &HandlerOptions{}
}
// ...
}
```
---
## 6. Interface-Based Pluggability
**Pattern name:** Interface Abstraction for Pluggable Implementations
**Source citation:** [crypto/crypto.go#L180](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L180), [net/http/transport.go#L66](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L66)
**What it does:** Core behavior is defined via an interface. The package provides
a default concrete implementation, but any user type satisfying the interface
can be substituted.
**Why:** Decouples high-level logic from low-level implementation. Enables testing
(mock transports), hardware integration (HSM-backed signers), and third-party
extensions without forking the package.
**Anti-pattern:** Concrete-type coupling everywhere; interfaces with too many methods
(hard to implement); accepting an interface but only ever using one implementation.
**Code example from source:**
```go
// crypto/crypto.go:180-200
// Signer is an interface for an opaque private key that can be used for
// signing operations. For example, an RSA key kept in a hardware module.
type Signer interface {
Public() PublicKey
Sign(rand io.Reader, digest []byte, opts SignerOpts) (signature []byte, err error)
}
```
```go
// net/http/transport.go (line 66+)
// Transport is an implementation of [RoundTripper] that supports HTTP,
// HTTPS, and HTTP proxies...
// Transports should be reused instead of created as needed.
// Transports are safe for concurrent use by multiple goroutines.
// net/http/client.go:57-58
type Client struct {
Transport RoundTripper // If nil, DefaultTransport is used.
// ...
}
```
---
## 7. Copy Protection via Dynamic Check
**Pattern name:** copyCheck (Runtime Copy Detection)
**Source citation:** [strings/builder.go#L32](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/strings/builder.go#L32)
**What it does:** On first mutation, the Builder records its own address. Subsequent
mutations compare the current receiver address against the recorded one. If they
differ, the struct was copied — it panics.
**Why:** Go has no language-level move semantics. For types where copying after first
use would cause data corruption or unsafe behavior (e.g., sharing an unsafe string
buffer), a runtime check is the pragmatic solution.
**Anti-pattern:** Silently allowing copies that corrupt state; using `sync.Mutex`-style
`noCopy` (vet catches it but it doesn't work for zero vs non-zero discrimination).
**Code example from source:**
```go
// strings/builder.go:32-40
func (b *Builder) copyCheck() {
if b.addr == nil {
b.addr = (*Builder)(abi.NoEscape(unsafe.Pointer(b)))
} else if b.addr != b {
panic("strings: illegal use of non-zero Builder copied by value")
}
}
```
---
## 8. DefaultXxx Singleton
**Pattern name:** Package-Level Default Instance
**Source citation:** [net/http/client.go#L109](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/client.go#L109), [net/http/transport.go#L47](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/transport.go#L47)
**What it does:** The package provides a pre-configured, ready-to-use instance as
a package-level variable. Package-level convenience functions delegate to it.
**Why:** Makes the simple case trivial (`http.Get(url)`) while allowing custom
instances for advanced use. Users never need to touch the defaults unless they
have specific requirements.
**Anti-pattern:** Forcing construction for basic use; not providing convenience
functions; making the default mutable in ways that affect all users.
**Code example from source:**
```go
// net/http/client.go:108-109
// DefaultClient is the default [Client] and is used by [Get], [Head], and [Post].
var DefaultClient = &Client{}
// net/http/transport.go:47-58
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
```
---
## 9. Functional Configuration via Method Chaining (Scanner Pattern)
**Pattern name:** Post-Construction Configuration via Methods
**Source citation:** [bufio/scan.go#L275](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/bufio/scan.go#L275)
**What it does:** After construction with `NewScanner`, optional configuration is
applied via methods (`Split`, `Buffer`) before the first call to `Scan`.
**Why:** Keeps the constructor minimal (only the required `io.Reader`). Optional
configuration is discoverable via methods. Panics if called after scanning starts
(enforcing a construction → configure → use lifecycle).
**Anti-pattern:** Trying to pass all options into the constructor; allowing
configuration changes mid-use that corrupt state.
**Code example from source:**
```go
// bufio/scan.go:275-293
// Buffer sets the initial buffer to use when scanning
// and the maximum size of buffer that may be allocated during scanning.
// ...
// Buffer panics if it is called after scanning has started.
func (s *Scanner) Buffer(buf []byte, max int) {
if s.scanCalled {
panic("Buffer called after Scan")
}
s.buf = buf
s.maxTokenSize = max
}
// Split sets the split function for the [Scanner].
// ...
// Split panics if it is called after scanning has started.
func (s *Scanner) Split(split SplitFunc) {
if s.scanCalled {
panic("Split called after Scan")
}
s.split = split
}
```
<!-- PATTERN_COMPLETE -->
-647
View File
@@ -1,647 +0,0 @@
# Code Style Patterns in the Go Standard Library
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
## 1. Naming Conventions: mixedCaps (No Underscores)
**Pattern name:** mixedCaps / MixedCaps
**Source citation:** All stdlib code (enforced by `gofmt` convention, documented in Effective Go)
**What it does:** All identifiers use mixedCaps (unexported) or MixedCaps (exported).
Underscores are never used in Go names except for test helpers and generated code.
**Why:** Consistent casing makes code scannable. The exported/unexported distinction
is communicated solely through initial capitalization — no separate `public`/`private`
keywords needed.
**Anti-pattern:** `snake_case` names; `ALL_CAPS` for constants; Hungarian notation
(`strName`, `iCount`).
**Code examples from source:**
```go
// net/http/server.go — exported
type Server struct { ... }
func ListenAndServe(addr string, handler Handler) error
// net/http/server.go — unexported
func (s *Server) shuttingDown() bool
const shutdownPollIntervalMax = 500 * time.Millisecond
```
---
## 2. Acronyms Are All-Caps
**Pattern name:** Acronym Capitalization
**Source citation:** [net/http/request.go#L130](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/request.go#L130) (`URL`), [net/http/server.go#L3040](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3040) (`TLSConfig`), [encoding/json/stream.go#L280](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/stream.go#L280) (`JSON`)
**What it does:** Acronyms and initialisms (URL, HTTP, ID, JSON, XML, HTML, TLS, TCP)
are always fully capitalized when exported, and fully lowercased when unexported.
**Why:** Consistency. `URL` not `Url`, `ID` not `Id`, `HTTP` not `Http`. This
applies even mid-word: `ServeHTTP`, `xmlEncoder`, `htmlEscape`.
**Anti-pattern:** `Url`, `Http`, `Json`, `Id` — mixing cases within an acronym.
**Code examples from source:**
```go
// net/http/request.go:130
URL *url.URL
// net/http/request.go:822
func ParseHTTPVersion(vers string) (major, minor int, ok bool)
// net/http/server.go:3040
TLSConfig *tls.Config
// encoding/json/stream.go:292
var _ Marshaler = (*RawMessage)(nil)
```
---
## 3. File Organization by Responsibility
**Pattern name:** One Concept Per File
**Source citation:** `net/http/` directory structure
**What it does:** Large packages split code into files by topic/type: `client.go`,
`server.go`, `transport.go`, `request.go`, `response.go`, `cookie.go`, `header.go`,
`fs.go`, `doc.go`. Each file is focused.
**Why:** Navigability. When you want to find client logic, you open `client.go`.
Files stay manageable sizes. Related code lives together.
**Anti-pattern:** One giant file with everything; splitting by access level
(`public.go` / `private.go`); splitting by method count rather than concept.
**File layout from `net/http/`:**
```
client.go — Client type and methods
transport.go — Transport type (low-level RoundTripper)
server.go — Server, Handler, ServeMux
request.go — Request type and parsing
response.go — Response type and reading
cookie.go — Cookie parsing and serialization
header.go — Header type and canonicalization
fs.go — FileServer, file serving
doc.go — Package documentation
clone.go — Clone helpers
method.go — HTTP method constants
pattern.go — URL pattern matching (ServeMux routing)
```
---
## 4. Blank Identifier for Interface Compliance
**Pattern name:** `var _ Interface = (*Type)(nil)`
**Source citation:** [io/io.go#L645](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L645), [os/file.go#L747](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L747), [encoding/json/stream.go#L280](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/stream.go#L280)
**What it does:** A package-level `var _ InterfaceName = (*ConcreteType)(nil)` declares
that the concrete type must satisfy the interface. The compiler verifies this at
build time.
**Why:** Catches interface drift at compile time without creating an instance. The
blank identifier discards the value — this is purely a static assertion.
**When to Use**
**Triggers:**
- You've defined a type that MUST satisfy an interface (implements `io.Reader`, `http.Handler`, etc.)
- You want a compile-time guarantee that catches drift when you add/remove methods
- You're writing a package with multiple types implementing the same interface
**Example — before:**
```go
type MyWriter struct{ buf bytes.Buffer }
func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }
// Months later, someone renames Write to WriteBuf...
// No compile error — only discovered at runtime when passed as io.Writer
```
**Example — after:**
```go
// Compile-time check: if MyWriter stops implementing io.Writer, this fails to build
var _ io.Writer = (*MyWriter)(nil)
type MyWriter struct{ buf bytes.Buffer }
func (w *MyWriter) Write(p []byte) (int, error) { return w.buf.Write(p) }
```
**When NOT to Use**
**Don't use interface compliance checks when:**
- The type is unexported AND only used locally (the compiler catches it at the call site anyway)
- You're asserting against an interface you don't own and it changes frequently (creates churn)
- The interface is implemented implicitly and you're just cargo-culting the pattern for every type
**Over-application example:**
```go
// Every single struct gets a compliance check, even trivial unexported ones
var _ fmt.Stringer = (*internalHelper)(nil) // only used in one function in this file
var _ fmt.Stringer = (*anotherHelper)(nil) // also never passed as Stringer anywhere
```
**Better alternative:**
```go
// Skip the check for types that are only used locally — the compiler
// will catch the issue at the point of use:
func printThing(s fmt.Stringer) { fmt.Println(s.String()) }
printThing(&internalHelper{}) // compiler error here if String() is missing
// DO use it for exported types that implement external interfaces:
var _ io.ReadWriteCloser = (*MyConnection)(nil) // users depend on this contract
```
**Why:** The pattern exists for API contracts — types that *must* satisfy an interface for consumers. For unexported types used only locally, the compiler already catches mismatches at the call site. Adding the check everywhere is noise.
**Anti-pattern:** Relying on tests to catch interface conformance; skipping the check
and discovering the mismatch at runtime; using reflection.
**Code examples from source:**
```go
// io/io.go:645
var _ ReaderFrom = discard{}
// os/file.go:747-750
var _ fs.StatFS = dirFS("")
var _ fs.ReadFileFS = dirFS("")
var _ fs.ReadDirFS = dirFS("")
var _ fs.ReadLinkFS = dirFS("")
// encoding/json/stream.go:292-293
var _ Marshaler = (*RawMessage)(nil)
var _ Unmarshaler = (*RawMessage)(nil)
// net/http/server.go:4071
var _ Pusher = (*timeoutWriter)(nil)
```
---
## 5. Named Return Values
**Pattern name:** Named Returns for Documentation (and Defer)
**Source citation:** [io/io.go#L87](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L87), 100, 314, 387; [os/file.go#L140](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/file.go#L140), 175
**What it does:** Return values are given names when the names add documentary value
(clarifying which int is what) or when `defer` needs to modify the return value.
**Why:** `(n int, err error)` is immediately understandable — `n` is the byte count.
Named returns also enable `defer func() { err = wrap(err) }()` patterns.
**Anti-pattern:** Naming returns for trivial functions where the types are
self-explanatory; using named returns as implicit variables throughout the function
body (confusing naked returns); always using naked `return` statements.
**Code examples from source:**
```go
// io/io.go:87 — Interface documentation
type Reader interface {
Read(p []byte) (n int, err error)
}
// io/io.go:100
type Writer interface {
Write(p []byte) (n int, err error)
}
// io/io.go:387 — Named return used with defer-style logic
func Copy(dst Writer, src Reader) (written int64, err error) {
return copyBuffer(dst, src, nil)
}
// os/file.go:140 — Named return for readability
func (f *File) Read(b []byte) (n int, err error) {
if err := f.checkValid("read"); err != nil {
return 0, err
}
n, e := f.read(b)
return n, f.wrapErr("read", e)
}
```
---
## 6. Defer for Resource Cleanup
**Pattern name:** `defer mu.Unlock()` / `defer f.Close()`
**Source citation:** [net/http/server.go#L3173](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L3173), [net/http/example_handle_test.go#L21](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L21)
**What it does:** Resources acquired at the top of a scope are immediately deferred
for cleanup. Mutexes are locked then immediately `defer Unlock()`'d.
**Why:** Guarantees cleanup regardless of return path (early returns, panics). Keeps
the acquire/release pair visually adjacent. Reduces bugs from forgotten unlocks.
**Anti-pattern:** Manual unlock at each return point; deferring in a loop (deferred
calls accumulate until function exit); deferring expensive operations that should
run earlier.
**Code examples from source:**
```go
// net/http/server.go:3171-3174
func (s *Server) Close() error {
s.inShutdown.Store(true)
s.mu.Lock()
defer s.mu.Unlock()
// ...
}
// net/http/example_handle_test.go:21-22
func (h *countHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.mu.Lock()
defer h.mu.Unlock()
h.n++
fmt.Fprintf(w, "count is %d\n", h.n)
}
```
---
## 7. Error Wrapping and Sentinel Errors
**Pattern name:** Sentinel Errors + Structured Error Types
**Source citation:** [os/error.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/error.go#L14), [os/error.go#L46](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/os/error.go#L46)
**What it does:** Package-level sentinel errors (`ErrNotExist`, `ErrPermission`) are
declared as `var` for use with `errors.Is()`. Structured error types (`*PathError`,
`*SyscallError`) carry context and implement `Unwrap()` for the errors chain.
**Why:** Enables programmatic error handling without string matching. `errors.Is(err, os.ErrNotExist)` works regardless of wrapping depth. Structured types let callers
extract the operation, path, or underlying syscall error.
**Anti-pattern:** Comparing error strings; creating unique error types for every
possible failure; not implementing `Unwrap`; sentinel errors as `const` (breaks
`errors.Is` for wrapped errors — use `var`).
**Code examples from source:**
```go
// os/error.go:14-27
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
)
// os/error.go:46
type PathError = fs.PathError
// os/error.go:49-57
type SyscallError struct {
Syscall string
Err error
}
func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }
func (e *SyscallError) Unwrap() error { return e.Err }
```
---
## 8. Receiver Naming: Short, Consistent, Never `this`/`self`
**Pattern name:** Single-Letter or Short Receiver Names
**Source citation:** All stdlib code; `net/http/server.go` uses `s` for Server, `bufio/scan.go` uses `s` for Scanner
**What it does:** Method receivers use 1–2 letter abbreviations of the type name,
consistent across all methods of that type: `s` for `*Server`, `b` for `*Builder`,
`f` for `*File`, `t` for `*Timer`.
**Why:** Receivers appear on every method. Short names reduce visual noise. Consistency
within a type avoids confusion. `this`/`self` are alien to Go's conventions.
**Anti-pattern:** `this`, `self`, `me`; long receiver names like `server`, `scanner`;
inconsistent receivers across methods of the same type.
**Code examples from source:**
```go
// net/http/server.go
func (s *Server) ListenAndServe() error { ... }
func (s *Server) Serve(l net.Listener) error { ... }
func (s *Server) Shutdown(ctx context.Context) error { ... }
// strings/builder.go
func (b *Builder) WriteString(s string) (int, error) { ... }
func (b *Builder) String() string { ... }
func (b *Builder) Grow(n int) { ... }
// os/file.go
func (f *File) Read(b []byte) (n int, err error) { ... }
func (f *File) Name() string { ... }
```
---
## 9. Constants: Typed, Grouped, with iota
**Pattern name:** Typed Constants with iota
**Source citation:** [crypto/crypto.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/crypto/crypto.go#L70), [time/time.go#L936](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L936)
**What it does:** Related constants are grouped in a `const ( ... )` block using
a named type and `iota` for sequential values. Constants of the same type
are exhaustively listed together.
**Why:** Type safety (can't accidentally pass an `os.Flag` where a `crypto.Hash` is
expected). `iota` eliminates magic numbers. Grouping makes the full set visible.
**When to Use**
**Triggers:**
- You have a set of related values that represent distinct states or options (status codes, modes, categories)
- Raw integers would be meaningless to readers (`SetMode(3)` vs `SetMode(ModeAsync)`)
- You want the type system to prevent passing a "color" where a "direction" is expected
**Example — before:**
```go
func SetLogLevel(level int) { ... }
// Caller:
SetLogLevel(3) // what does 3 mean?!
SetLogLevel(-1) // valid? who knows
```
**Example — after:**
```go
type LogLevel int
const (
LevelDebug LogLevel = iota
LevelInfo
LevelWarn
LevelError
)
func SetLogLevel(level LogLevel) { ... }
// Caller:
SetLogLevel(LevelWarn) // self-documenting
```
**When NOT to Use**
**Don't use typed constants with iota when:**
- The values have external meaning (HTTP status codes, exit codes, protocol bytes) — use explicit values
- The set is not exhaustive or will have gaps (iota assigns sequential values; gaps require explicit assignment)
- You need the constant to interoperate with untyped int APIs without casting everywhere
**Over-application example:**
```go
type HTTPStatus int
const (
StatusOK HTTPStatus = iota // 0?! HTTP 200 is not 0
StatusNotFound // 1?! Should be 404
StatusServerError // 2?! Should be 500
)
```
**Better alternative:**
```go
type HTTPStatus int
const (
StatusOK HTTPStatus = 200
StatusNotFound HTTPStatus = 404
StatusServerError HTTPStatus = 500
)
```
**Why:** `iota` is for sequential enumerations where the actual numeric value doesn't matter (only the distinctness matters). When values have external meaning (wire protocols, HTTP, exit codes), use explicit values.
**Anti-pattern:** Untyped numeric constants; separate `const` declarations for related
values; using raw integers in function signatures.
**Code examples from source:**
```go
// crypto/crypto.go:70-85
const (
MD4 Hash = 1 + iota
MD5
SHA1
SHA224
SHA256
// ...
)
// time/time.go:934-942
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
```
---
## 10. Comments: Guard Clauses Over Conditions
**Pattern name:** `// guards x` Field Comments
**Source citation:** [net/http/example_handle_test.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/example_handle_test.go#L16)
**What it does:** When a sync primitive (mutex) protects specific fields, a brief
comment documents what it guards: `mu sync.Mutex // guards n`.
**Why:** Concurrency bugs come from unclear ownership. A one-line comment makes the
lock's scope obvious to every reader.
**Anti-pattern:** No documentation of what a lock protects; locks that protect
"everything" (unclear scope); comments that restate the type.
**Code example from source:**
```go
// net/http/example_handle_test.go:16-17
type countHandler struct {
mu sync.Mutex // guards n
n int
}
```
---
## 11. Duration Type Pattern
**Pattern name:** Named Type for Semantic Units
**Source citation:** [time/time.go#L915](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/time/time.go#L915)
**What it does:** `Duration` is `type Duration int64` — a named type over a primitive.
This gives it its own method set (`String()`, `Hours()`, `Truncate()`) and prevents
accidental mixing with raw int64 values.
**Why:** Semantic meaning through the type system. You can't accidentally pass
nanoseconds where seconds are expected. Methods provide conversion and formatting.
Constants like `time.Second` make intent clear.
**When to Use**
**Triggers:**
- A primitive type (`int`, `string`, `float64`) has a specific **semantic meaning** in your domain
- You want to attach methods (formatting, validation, arithmetic) to the value
- You've seen bugs from accidentally mixing units (`int` could be seconds, milliseconds, or nanoseconds)
**Example — before:**
```go
func SetTimeout(ms int) { ... } // is this milliseconds? seconds?
func SetRetries(n int) { ... } // can't accidentally swap these... or CAN you?
SetTimeout(5) // 5 what?
SetRetries(500) // oops, swapped arguments — compiles fine!
```
**Example — after:**
```go
type Timeout time.Duration
type RetryCount int
func SetTimeout(t Timeout) { ... }
func SetRetries(n RetryCount) { ... }
SetTimeout(Timeout(5 * time.Second)) // explicit units
SetRetries(RetryCount(3)) // can't swap — different types
```
**When NOT to Use**
**Don't create named types when:**
- The primitive is only used in one place (over-engineering for a single call site)
- The type would need constant casting back to the primitive for stdlib interop
- The semantic meaning is already clear from the parameter name (`func Sleep(seconds int)` in a script is fine)
**Over-application example:**
```go
type Port uint16
type Host string
type Path string
// Now every function that takes these needs explicit construction:
func Connect(h Host, p Port, path Path) { ... }
Connect(Host("localhost"), Port(8080), Path("/api")) // ceremony for no safety gain
```
**Better alternative:**
```go
// For simple configurations, a struct with named fields provides clarity without type ceremony:
type Endpoint struct {
Host string
Port uint16
Path string
}
func Connect(ep Endpoint) { ... }
Connect(Endpoint{Host: "localhost", Port: 8080, Path: "/api"})
```
**Why:** Named types shine when you need methods, when confusion between units causes real bugs (seconds vs milliseconds), or when the type system should prevent mixing semantically different values of the same primitive. If you're just adding type annotations to strings that don't interact, you're adding ceremony without safety.
**Anti-pattern:** Using raw `int64` for durations; accepting `int` parameters for
time intervals; mixing units (milliseconds in one place, seconds in another).
**Code example from source:**
```go
// time/time.go:915
type Duration int64
// time/time.go:947-949
func (d Duration) String() string {
var arr [32]byte
n := d.format(&arr)
return string(arr[n:])
}
```
---
## 12. gofmt: Non-Negotiable Formatting
**Pattern name:** Canonical Formatting via gofmt
**Source citation:** Every file in the Go standard library
**What it does:** All Go code is formatted with `gofmt`. Tabs for indentation, spaces
for alignment. No style debates — the tool decides.
**Why:** Eliminates formatting bikesheds. All Go code looks the same regardless of
author. Diffs show only semantic changes, never style changes. Tooling can parse
and emit canonical code.
**Anti-pattern:** Manual formatting; spaces for indentation; custom alignment rules;
checking in code that `gofmt` would modify.
**Key rules enforced by gofmt:**
- Tabs for indentation
- Opening brace on the same line (`if x {`)
- No optional parentheses (`if x`, not `if (x)`)
- Aligned struct field tags
- One blank line between top-level declarations
- No trailing whitespace
---
## 13. Import Organization
**Pattern name:** Grouped Imports (stdlib / external / internal)
**Source citation:** [net/http/server.go#L9](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L9)
**What it does:** Imports are organized in groups separated by blank lines:
1. Standard library
2. External packages (golang.org/x, third-party)
3. Internal packages
The `goimports` tool enforces this automatically.
**Why:** Scannable at a glance. Makes dependency provenance clear (stdlib vs.
external). Reduces merge conflicts.
**Code example from source:**
```go
// net/http/server.go:9-36
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
// ... more stdlib ...
"time"
_ "unsafe" // for linkname
"golang.org/x/net/http/httpguts"
)
```
<!-- PATTERN_COMPLETE -->
+14 -264
View File
@@ -1,7 +1,5 @@
# Advanced Go Testing Patterns
**Source:** [golang/go](https://github.com/golang/go) at commit [`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
Patterns extracted from the Go standard library (`src/net/http/`, `src/encoding/json/`, `src/testing/`) and Kubernetes source code.
---
@@ -12,104 +10,12 @@ The canonical Go test style. Every Go stdlib test file uses this pattern.
### Pattern Name: Anonymous Struct Test Table
**Source:** [src/net/http/header_test.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/header_test.go#L17)
**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.
**When to Use**
**Triggers:**
- You're testing a function with many input/output combinations
- You're copy-pasting test functions that differ by one or two values
- Adding a new test case requires duplicating 10+ lines of setup/assertion code
**Example — before:**
```go
func TestParseSize(t *testing.T) {
result1, err1 := ParseSize("10MB")
if err1 != nil || result1 != 10_000_000 { t.Error("10MB failed") }
result2, err2 := ParseSize("1GB")
if err2 != nil || result2 != 1_000_000_000 { t.Error("1GB failed") }
result3, err3 := ParseSize("invalid")
if err3 == nil { t.Error("invalid should fail") }
// ... 20 more copy-pasted blocks
}
```
**Example — after:**
```go
func TestParseSize(t *testing.T) {
tests := []struct {
input string
want int64
wantErr bool
}{
{"10MB", 10_000_000, false},
{"1GB", 1_000_000_000, false},
{"invalid", 0, true},
{"0B", 0, false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got, err := ParseSize(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("ParseSize(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr)
}
if got != tt.want {
t.Errorf("ParseSize(%q) = %d, want %d", tt.input, got, tt.want)
}
})
}
}
```
### When NOT to Use
**Don't use this when:**
- You have 1–2 test cases with significantly different setup logic — a table adds indirection for no gain
- Each case requires unique assertions or error-checking logic that can't be unified
- The test is inherently sequential (step 2 depends on step 1's output)
**Over-application example:**
```go
func TestMigration(t *testing.T) {
tests := []struct {
name string
// ... 15 fields for setup, teardown, assertions, side effects
}{
{"migrate v1 to v2", /* massive struct literal */},
{"migrate v2 to v3", /* completely different struct literal */},
}
for _, tt := range tests {
// 50 lines of conditional logic because each case is fundamentally different
}
}
```
**Better alternative:**
```go
func TestMigrateV1ToV2(t *testing.T) {
// Clear, self-contained, readable
db := setupV1(t)
err := MigrateToV2(db)
// specific assertions for this migration
}
func TestMigrateV2ToV3(t *testing.T) {
db := setupV2(t)
err := MigrateToV3(db)
// different assertions entirely
}
```
**Why:** Table-driven tests shine when cases share identical setup/assertion logic and differ
only in inputs and expected outputs. When each "case" needs its own control flow, the table
becomes a mini-DSL that's harder to read than separate functions.
**Anti-pattern:** Writing individual assertions for each case, or copy-pasting test functions that differ by one input.
**Code example (stdlib):**
@@ -147,7 +53,7 @@ func TestHeaderWrite(t *testing.T) {
### Pattern Name: Named Table Tests with t.Run (Subtests)
**Source:** [src/encoding/json/encode_test.go#L285](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode_test.go#L285), [src/encoding/json/scanner_test.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/scanner_test.go#L30)
**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.
@@ -182,7 +88,7 @@ func TestValid(t *testing.T) {
### Pattern Name: CaseName with Caller Position Tracking
**Source:** [src/encoding/json/internal/jsontest/testcase.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/internal/jsontest/testcase.go#L18)
**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.
@@ -216,7 +122,7 @@ func (pos CasePos) String() string {
### Pattern Name: t.Helper() for Clean Stack Traces
**Source:** [src/testing/testing.go#L1415](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing.go#L1415)
**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.
@@ -256,7 +162,7 @@ func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
### Pattern Name: *testing.T as First Argument to Helpers
**Source:** [src/net/http/serve_test.go#L4555](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L4555)
**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.
@@ -290,7 +196,7 @@ mustGet := func(url string, headers ...string) {
### Pattern Name: t.Cleanup for Test-Scoped Resources
**Source:** [src/testing/testing.go#L1439](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing.go#L1439), [src/net/http/clientserver_test.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L120)
**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.
@@ -350,7 +256,7 @@ ServeFile(w, r, "testdata/file")
### Pattern Name: Golden Files with -update Flag
**Source:** [src/cmd/gofmt/gofmt_test.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/cmd/gofmt/gofmt_test.go#L18), 113-138
**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.
@@ -360,89 +266,6 @@ ServeFile(w, r, "testdata/file")
3. Golden files serve as documentation of expected behavior.
4. Reviewers can see exactly what output changed in diffs.
**When to Use**
**Triggers:**
- Your function produces complex multi-line output (formatted code, HTML, JSON, error messages)
- Expected output would be 20+ lines if inlined in the test — unreadable
- Output changes intentionally sometimes and you need a quick way to approve the new version
**Example — before:**
```go
func TestRenderTemplate(t *testing.T) {
got := renderHTML(data)
want := `<!DOCTYPE html>
<html>
<head><title>Hello</title></head>
<body>
<h1>Welcome, Alice</h1>
<p>You have 3 messages.</p>
</body>
</html>` // 8 lines inline — and this is a SIMPLE template
if got != want { t.Errorf("mismatch") }
}
```
**Example — after:**
```go
var update = flag.Bool("update", false, "update golden files")
func TestRenderTemplate(t *testing.T) {
got := renderHTML(data)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, []byte(got), 0644)
return
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Errorf("output mismatch; run with -update to accept new output")
}
}
// Golden file lives at testdata/TestRenderTemplate.golden
```
### When NOT to Use
**Don't use this when:**
- Expected output is short (< 5 lines) — inline it directly for readability
- Output is non-deterministic (timestamps, random IDs, goroutine ordering) without normalization
- The golden file would need updating on every minor refactor — brittle and noisy diffs
**Over-application example:**
```go
// Golden file for a one-line output
var update = flag.Bool("update", false, "update golden files")
func TestVersion(t *testing.T) {
got := Version()
golden := "testdata/TestVersion.golden"
if *update {
os.WriteFile(golden, []byte(got), 0644)
return
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Error("mismatch")
}
}
// testdata/TestVersion.golden contains: "v1.2.3" — seriously?
```
**Better alternative:**
```go
func TestVersion(t *testing.T) {
got := Version()
if got != "v1.2.3" {
t.Errorf("Version() = %q, want %q", got, "v1.2.3")
}
}
```
**Why:** Golden files add process overhead (the `-update` workflow, reviewing diffs in a separate
file). For short, stable outputs, inline comparison is simpler, faster to read, and keeps the
expected value next to the assertion.
**Anti-pattern:** Comparing against inline expected strings that span 50+ lines, or manually constructing expected output.
**Code example (stdlib):**
@@ -490,83 +313,12 @@ func TestRewrite(t *testing.T) {
### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers
**Source:** [src/net/http/serve_test.go#L387](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L387)
**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.
**When to Use**
**Triggers:**
- You're testing HTTP handler logic (status codes, headers, response body) in isolation
- You don't need real TCP connections, TLS, or routing
- Your test should run in <1ms, not wait for port binding
**Example — before:**
```go
func TestHealthHandler(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(healthHandler))
defer srv.Close()
resp, _ := http.Get(srv.URL + "/health") // real TCP connection — slow
if resp.StatusCode != 200 { t.Fatal("not healthy") }
}
```
**Example — after:**
```go
func TestHealthHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
healthHandler(rec, req) // direct call — no network
if rec.Code != 200 {
t.Fatalf("got status %d, want 200", rec.Code)
}
if rec.Body.String() != "ok" {
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
}
}
```
### When NOT to Use
**Don't use this when:**
- You need to test real HTTP behavior: TLS handshakes, connection pooling, timeouts, keep-alive
- Your handler depends on server-level middleware (e.g., `http.Server.ConnContext`, TLS client certs)
- You're testing client behavior or redirect-following (need a real URL to connect to)
**Over-application example:**
```go
func TestClientRetries(t *testing.T) {
rec := httptest.NewRecorder()
// Can't test retry logic — there's no real server for the client to connect to!
// rec doesn't have a URL, no TCP, no connection reset simulation
}
```
**Better alternative:**
```go
func TestClientRetries(t *testing.T) {
attempts := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
attempts++
if attempts < 3 {
w.WriteHeader(503)
return
}
w.WriteHeader(200)
}))
defer srv.Close()
// Now test the client's retry behavior against a real server
resp, err := myClient.Get(srv.URL + "/resource")
// ...
}
```
**Why:** `httptest.NewRecorder` tests handler logic in isolation — it has no network, no URL,
no connection lifecycle. When you need to test anything that crosses the network boundary
(clients, retries, TLS, timeouts), you need `httptest.NewServer`.
**Anti-pattern:** Spinning up a full server to test handler logic that doesn't need networking.
**Code example (stdlib):**
@@ -593,7 +345,7 @@ func TestServeMuxHandler(t *testing.T) {
### Pattern Name: httptest.NewServer for Integration-Style Tests
**Source:** [src/net/http/clientserver_test.go#L203](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L203)
**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.
@@ -624,7 +376,7 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c
### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes
**Source:** [src/encoding/json/bench_test.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/bench_test.go#L85)
**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.
@@ -662,7 +414,7 @@ func BenchmarkCodeEncoder(b *testing.B) {
### Pattern Name: testing.Short() for Expensive Tests
**Source:** [src/net/http/serve_test.go#L800](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L800), 1000, 2212, 2581
**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.
@@ -756,7 +508,7 @@ func afterTest(t testing.TB) {
### Pattern Name: Bridge File for Internal Testing
**Source:** [src/net/http/export_test.go#L1](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/export_test.go#L1)
**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.
@@ -781,7 +533,7 @@ var (
### Pattern Name: Generic Test Runner Across Protocol Modes
**Source:** [src/net/http/clientserver_test.go#L100](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L100)
**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.
@@ -812,7 +564,7 @@ func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
### Pattern Name: io.Writer Adapter for *testing.T
**Source:** [src/net/http/clientserver_test.go#L337](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L337)
**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).
@@ -832,5 +584,3 @@ func (w testLogWriter) Write(b []byte) (int, error) {
// Usage:
cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0)
```
<!-- PATTERN_COMPLETE -->
+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"` |
-473
View File
@@ -1,473 +0,0 @@
# Anti-Patterns: What Go's Stdlib Avoids (and Why)
Patterns the Go standard library team actively avoids, extracted
from studying what they DON'T do in their source code.
**Source:** [golang/go](https://github.com/golang/go) at commit
[`17bd5ab`](https://github.com/golang/go/tree/17bd5ab8c650155dd2bd09f7005726552639eea0)
---
## 1. Returning Errors and Values Simultaneously
**What they avoid:** Functions that return a valid value alongside
a non-nil error.
**Source evidence:** The entire stdlib follows: if error is non-nil,
other return values are undefined/zero. `io.Reader.Read` is the ONE
exception (documented as "n > 0 AND err possible").
**Why it's bad:** Callers check the error OR use the value — not both.
If you return data with an error, callers might ignore the error
because they got "valid" data.
```go
// BAD — ambiguous: is result valid when err != nil?
func fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return partialData, err // caller might use partialData without checking err
}
return io.ReadAll(resp.Body)
}
// GOOD — nil data when error is non-nil
func fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, err
}
return io.ReadAll(resp.Body)
}
```
### When to Apply This Rule
**Triggers:**
- Returning non-zero values alongside non-nil errors
- Callers that use the value without checking the error
### Exceptions
- `io.Reader.Read()` — explicitly documented as "n > 0 AND err == io.EOF" case
- Functions that return "best effort" partial results (document this clearly)
---
## 2. Large Interfaces (Java-Style)
**What they avoid:** Interfaces with more than 1-3 methods.
**Source evidence:** `io.Reader` (1 method), `io.Writer` (1 method),
`fmt.Stringer` (1 method), `sort.Interface` (3 methods). The largest
stdlib interface is `net.Conn` at 8 methods — and it's widely
considered too large.
**Why it's bad:** Large interfaces are hard to implement (fewer types
satisfy the contract). Small interfaces compose: `io.ReadWriter` is
just `Reader + Writer`. The bigger the interface, the tighter the
coupling.
```go
// BAD — Java-style "service" interface
type UserService interface {
Create(ctx context.Context, u *User) error
Get(ctx context.Context, id string) (*User, error)
Update(ctx context.Context, u *User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, filter Filter) ([]*User, error)
Count(ctx context.Context) (int, error)
Search(ctx context.Context, q string) ([]*User, error)
}
// Only ONE implementation will ever satisfy this. It's a class in disguise.
// GOOD — small, composable interfaces
type UserReader interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
SaveUser(ctx context.Context, u *User) error
}
// Functions accept the smallest interface they need
func sendWelcome(ctx context.Context, r UserReader, id string) error { ... }
```
### When to Apply This Rule
**Triggers:**
- Interface with 4+ methods
- Only one implementation exists
- Interface defined in the same package as the implementation
### Exceptions
- Interfaces matching an external protocol (net.Conn — mirrors TCP API)
- Well-known stdlib patterns (sort.Interface — exactly 3 methods)
---
## 3. Package-Level init() for Complex Logic
**What they avoid:** Using `init()` for anything beyond simple
registration or flag setup.
**Source evidence:** Stdlib init() functions are exclusively used for:
registering encodings, initializing hash algorithms, setting up flags.
Never for: opening files, making network calls, or complex computation.
**Why it's bad:** init() runs at import time — you can't control when.
Can't pass parameters. Can't return errors. Can't skip in tests.
Hard to debug (runs before main). Creates import side effects.
```go
// BAD — complex logic in init
func init() {
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err) // crashes the program at import time
}
globalDB = db
}
// GOOD — explicit initialization
func NewApp(dsn string) (*App, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
return &App{db: db}, nil
}
```
### When to Apply This Rule
**Triggers:**
- init() that opens files, network connections, or databases
- init() with error handling (log.Fatal/panic)
- init() that takes >5 lines
- init() that depends on environment variables
### Exceptions
- Registering encodings: `encoding/gob`, `image/png` init registration
- Seeding randomness or initializing lookup tables
- Flag registration (`flag.String(...)` in init)
---
## 4. Stuttering Names
**What they avoid:** Package-qualified names that repeat the package
name: `http.HTTPServer`, `user.UserService`, `json.JSONEncoder`.
**Source evidence:** The stdlib uses: `http.Server` (not HTTPServer),
`json.Encoder` (not JSONEncoder), `bytes.Buffer` (not BytesBuffer).
The package name provides context.
**Why it's bad:** When you import and use `http.HTTPServer`, you say
"HTTP" twice. Go's package system already provides namespace context.
Stuttering is visual noise that adds no information.
```go
// BAD — stuttering
package user
type UserService struct { ... } // user.UserService
type UserRepository interface { ... } // user.UserRepository
func NewUserService() *UserService { ... }
// GOOD — the package name provides context
package user
type Service struct { ... } // user.Service
type Repository interface { ... } // user.Repository
func NewService() *Service { ... }
```
### When to Apply This Rule
**Triggers:**
- Type name starts with the package name
- `New<PackageName><Type>` constructor pattern
- Reading the qualified name aloud sounds redundant
### Exceptions
- When omitting the prefix would be ambiguous (`error.Error` — Go uses this)
- Single-package binaries where qualification doesn't apply
---
## 5. Naked Returns in Long Functions
**What they avoid:** Using named return values with bare `return`
in functions longer than a few lines.
**Source evidence:** The stdlib uses naked returns only in very short
functions (1-5 lines). Longer functions always return explicit values.
The `go vet` tool warns about this.
**Why it's bad:** In a 50-line function with naked return, you have
to trace every assignment to the named returns to understand what's
being returned. It's like having invisible variables.
```go
// BAD — naked return in long function
func process(data []byte) (result int, err error) {
// ... 40 lines of code ...
if something {
result = compute()
return // what is err here? was it set earlier? who knows
}
// ... more code ...
return // mystery values
}
// GOOD — explicit returns
func process(data []byte) (int, error) {
// ... 40 lines of code ...
if something {
return compute(), nil // clear what's being returned
}
return 0, fmt.Errorf("processing: %w", err)
}
```
### When to Apply This Rule
**Triggers:**
- Function body > 10 lines with naked returns
- Multiple return points with naked returns
- Named returns used ONLY for naked return (not for documentation)
### Exceptions
- Short functions (3-5 lines) where named returns add clarity
- Defer-based error wrapping: `defer func() { err = wrap(err) }()`
(this is the primary legitimate use of named returns)
---
## 6. Error String Formatting (Capitalization/Punctuation)
**What they avoid:** Error messages that start with capitals or end
with punctuation.
**Source evidence:** Every error string in the stdlib is lowercase,
no trailing period. `fmt.Errorf("open %s: %w", path, err)` — never
`"Failed to open file."`.
**Why it's bad:** Errors compose. `fmt.Errorf("connect: %w", err)`
produces `connect: open /etc/hosts: permission denied`. If inner
errors are capitalized or punctuated, the chain looks broken:
`connect: Failed to open file.`
```go
// BAD — capitalized, punctuated
return fmt.Errorf("Failed to open configuration file: %s.", path)
// GOOD — lowercase, no punctuation, wraps cleanly
return fmt.Errorf("open config %s: %w", path, err)
```
### When to Apply This Rule
**Triggers:**
- Error strings starting with uppercase
- Error strings ending with `.` or `!`
- Error messages that don't compose well with wrapping
### Exceptions
- Proper nouns: `"Google API returned 503"`
- Acronyms at the start: `"DNS lookup failed"` (some teams accept this)
---
## 7. Overusing Channels (When Mutex Suffices)
**What they avoid:** Using channels for simple mutual exclusion
where `sync.Mutex` would be clearer.
**Source evidence:** The stdlib uses Mutex 540 times vs channels
for synchronization ~50 times. Channels are for communication
between goroutines. Mutex is for protecting shared state.
**Why it's bad:** A channel of capacity 1 as a mutex is clever
but obscure. It adds cognitive load, makes locking/unlocking
asymmetric, and doesn't have `defer mu.Unlock()` ergonomics.
```go
// BAD — channel as mutex (clever but obscure)
sem := make(chan struct{}, 1)
sem <- struct{}{} // "lock"
// ... critical section ...
<-sem // "unlock"
// GOOD — mutex is clear and idiomatic
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
// ... critical section ...
```
### When to Apply This Rule
**Triggers:**
- `make(chan struct{}, 1)` used as a lock
- Channel send/receive pairs that don't actually communicate data
- Complex select statements where a simple lock would work
### Exceptions
- Rate limiting (semaphore with capacity > 1)
- Signaling between goroutines (cancellation, done channels)
- Fan-out/fan-in patterns (actual communication)
---
## 8. Accepting Interfaces, Returning Interfaces
**What they avoid:** Returning interfaces from functions (except
well-known ones like `error`).
**Source evidence:** The Go proverb: "Accept interfaces, return structs."
The stdlib returns concrete types: `http.NewServeMux()` returns
`*ServeMux`, not `http.Handler`. This allows callers to access
all methods.
**Why it's bad:** Returning an interface hides the concrete type.
Callers can't access methods that aren't in the interface. Makes
testing harder (can't construct the concrete type). Prevents
future method additions without breaking the interface.
```go
// BAD — returns interface (hides concrete type)
func NewCache() Cacher {
return &redisCache{...}
}
// Callers can't access redis-specific methods or fields
// GOOD — returns concrete type
func NewCache() *RedisCache {
return &RedisCache{...}
}
// Callers get the full type. They can still use it as Cacher where needed.
```
### When to Apply This Rule
**Triggers:**
- Factory functions returning interfaces
- Single-implementation interfaces returned from constructors
- Interface return types that prevent callers from accessing useful methods
### Exceptions
- Standard error returns (`error` interface)
- When there genuinely are multiple implementations chosen at runtime
- Plugin systems where the concrete type must be hidden
---
## 9. Panic in Library Code
**What they avoid:** Using `panic()` for conditions callers could
handle.
**Source evidence:** stdlib panic usage is exclusively for: (1) index
out of bounds, (2) nil pointer on documented non-nil parameter,
(3) impossible states (unreachable code). Never for "file not found"
or "invalid input" style errors.
**Why it's bad:** panic kills the goroutine (and the program unless
recovered). Library code shouldn't decide that an error is fatal —
that's the caller's decision.
```go
// BAD — panicking on recoverable error
func MustParse(s string) Config {
cfg, err := Parse(s)
if err != nil {
panic(err) // caller has no way to handle bad input
}
return cfg
}
// GOOD — return error, let caller decide
func Parse(s string) (Config, error) {
// ... parse logic ...
if invalid {
return Config{}, fmt.Errorf("parse config: %w", err)
}
return cfg, nil
}
```
### When to Apply This Rule
**Triggers:**
- `panic()` in exported functions on user input
- `Must*` functions without a non-panicking alternative
- `log.Fatal` in library code (same as panic — kills the process)
### Exceptions
- `Must*` pattern for compile-time constants: `regexp.MustCompile("^\\d+$")`
- Truly impossible states after exhaustive validation
- Test helpers that panic on setup failure
---
## 10. Unexported Interface Satisfaction
**What they avoid:** Defining interfaces in the implementor's package
rather than the consumer's package.
**Source evidence:** The Go wiki explicitly says: "Define interfaces
in the package that USES them, not the package that implements them."
The stdlib follows this — `io.Reader` is in `io` (consumer), not
in `os` or `net` (implementors).
**Why it's bad:** Pre-declaring interfaces in the implementor package
couples consumer to producer. It's Java-style "implements Readable"
thinking. In Go, satisfaction is implicit — the consumer defines
what it needs.
```go
// BAD — interface in the implementation package
package storage
type Store interface { // defined where it's implemented
Get(key string) ([]byte, error)
Set(key string, val []byte) error
}
type RedisStore struct { ... }
// Now everyone who imports storage gets this interface forced on them
// GOOD — interface in the consumer package
package handler
type Getter interface { // defined where it's used
Get(key string) ([]byte, error)
}
func NewHandler(store Getter) *Handler { ... }
// handler only depends on what it actually needs
```
### When to Apply This Rule
**Triggers:**
- Interface defined next to its only implementation
- Interface in the same package as the struct that satisfies it
- Interface with methods matching exactly one struct
### Exceptions
- Well-known "standard" interfaces (io.Reader, sort.Interface)
- Interfaces that define a protocol many packages implement
- Plugin/driver interfaces where the contract IS the package's purpose
<!-- PATTERN_COMPLETE -->
+436 -524
View File
File diff suppressed because it is too large Load Diff
-15
View File
@@ -1,15 +0,0 @@
# Sources
Reference material extracted from specific projects. Study for ideas, don't copy blindly.
These are **descriptive** — they document what a project does and why.
The `patterns/` directory is **prescriptive** — it tells you what to do.
Patterns that prove broadly applicable get promoted from here into `patterns/`.
The rest stays as reference for understanding how mature projects solve specific problems.
## Files
- `golang.md` — conventions from the golang/go source
- `golang-analysis.md` — deeper analysis of golang/go source architecture
- `prometheus.md` — patterns from prometheus/prometheus (TSDB, storage, metrics)
-364
View File
@@ -1,364 +0,0 @@
# Go Language Source: Architectural Conventions
How does the Go team build Go itself? What does the language source
reveal about conventions, governance, and infrastructure decisions?
**Repo:** [golang/go](https://github.com/golang/go)
---
## 1. Repo Shape
| Metric | Value |
|--------|-------|
| Size | 632M |
| Go files | 11,245 |
| Assembly files (runtime) | 200 |
| Commits | 66,142 |
| Contributors | 2,842 |
| Test files | 1,811 |
| Non-test files | 6,065 |
| Test ratio | 1:3.3 |
| TODOs (non-test) | 3,428 (all owner-attributed) |
### Organizational Philosophy
```
src/
├── cmd/ # The toolchain (compile, link, go, gofmt, vet)
│ └── compile/
│ └── internal/ssa/ # 417,686 lines of SSA compiler
├── internal/ # 61 hidden packages (API firewall)
├── runtime/ # 129,141 lines (scheduler, GC, memory)
├── encoding/ # Serialization (json, xml, gob)
├── net/ # Networking
├── io/ # Stream interfaces
└── testing/ # Test framework
```
The Go source has three layers:
1. **Flat stdlib** — user-visible packages (`fmt`, `io`, `net`)
2. **`internal/`** — shared infrastructure hidden from users (61 packages)
3. **`cmd/`** — the toolchain itself (compiler, linker, build tool)
---
## 2. What the Codebase Values
### By import frequency
| Package | Imports | Role |
|---------|---------|------|
| `fmt` | 2,031 | Formatting (the universal tool) |
| `testing` | 1,658 | Tests are first-class citizens |
| `strings` | 1,454 | String manipulation |
| `os` | 1,306 | System interaction |
| `unsafe` | 1,304 | Low-level memory access |
| `runtime` | 970 | Runtime introspection |
| `io` | 924 | Stream abstraction |
**The surprise:** `unsafe` (1,304 imports) ranks 5th — nearly tied with
`os`. The language that preaches memory safety uses `unsafe` extensively
in its own implementation. This is the "know the rules so you know
where they don't apply" principle.
### By size (what consumes the most lines)
| Component | Lines | Nature |
|-----------|-------|--------|
| SSA compiler | 417,686 | Mostly generated rewrite rules |
| Runtime | 129,141 | Scheduler + GC + memory |
| `testing/` | 4,770 | Testing framework |
| `internal/poll` | 5,087 | OS I/O multiplexing |
| `internal/fuzz` | 4,220 | Fuzzing infrastructure |
| `encoding/json/v2` | 6,387 | The JSON rewrite (experiment) |
---
## 3. The Bootstrap Problem
**How does Go compile itself?**
Go has been self-hosting since May 2015 (Russ Cox, commit `0f4132c907`:
"all: build and use go tool compile, go tool link"). The bootstrap
chain:
1. A pre-built Go 1.4 binary compiles the bootstrap tools
2. Those tools compile the current Go toolchain
3. The new toolchain recompiles itself for correctness
**The `cmd/dist` tool** orchestrates this multi-stage build. Unlike
Elixir (which keeps 33 Erlang files permanently), Go eliminated its
non-Go dependencies entirely. The only external requirement is a
previous Go binary.
**Convention:** Self-hosting means the compiler, linker, and runtime
are all written in Go. The runtime includes 200 assembly files for
architecture-specific operations (scheduler context switches, atomic
operations, system calls).
---
## 4. TODO Culture: Owned Accountability
```go
// TODO(gri) — 320 occurrences (Robert Griesemer)
// TODO(mdempsky) — 198 occurrences (Matthew Dempsky)
// TODO(adonovan) — 170 occurrences (Alan Donovan)
// TODO(mknyszek) — 98 occurrences (Michael Knyszek)
// TODO(rsc) — 96 occurrences (Russ Cox)
```
**3,428 TODOs** in non-test code. Every TODO has an owner. The top 5
TODO authors are all core team members (compiler, runtime, and tools
leads).
**Convention:** `// TODO(username): description`
**Growth pattern:** linkname directives — 43 touches in 2019, 48 in
2022, 92 in 2024, 72 in 2025. The codebase is actively evolving its
relationship with internal/external boundaries.
**Contrast with Elixir:** Go TODOs are permanent documentation of known
limitations. Elixir TODOs are time-bombs with version deadlines. Go
accepts technical debt as a layer; Elixir refuses to let it accumulate.
---
## 5. Unique Patterns
### 5.1 `internal/` as API Firewall (61 packages)
Go's most distinctive structural pattern. The `internal/` directory is
enforced by the compiler — code outside the tree cannot import these
packages.
**Key internal packages:**
- `internal/godebug` — runtime feature flags (79 settings)
- `internal/goexperiment` — compile-time experiment guards
- `internal/singleflight` — dedup concurrent function calls
- `internal/bisect` — binary search for debugging (Russ Cox, 2023)
- `internal/poll` — OS I/O polling (5,087 lines)
- `internal/fuzz` — fuzzing coordinator (4,220 lines)
- `internal/coverage` — code coverage (3,821 lines)
**Convention:** Code shared between stdlib packages but not suitable for
public API goes in `internal/`. This is Go's answer to "how do you
share utilities without committing to backward compatibility."
### 5.2 GODEBUG: Runtime Feature Flags
Introduced 2021 (Brad Fitzpatrick), formalized as a compatibility
mechanism by Russ Cox in 2022 (proposal #56986: "extended backwards
compatibility for Go", 70 comments).
```go
var http2server = godebug.New("http2server")
func ServeConn(c net.Conn) {
if http2server.Value() == "0" {
// disable HTTP/2
}
}
```
**79 godebug settings** across the codebase. Each time a non-default
setting causes a behavior change, code calls `IncNonDefault()` to
increment a counter readable via `runtime/metrics`.
**Convention:** When a behavior change might break existing programs,
add a GODEBUG setting. The old behavior remains accessible via
`GODEBUG=setting=old_value`. New Go versions automatically use the
old behavior when building code that declared an older `go` directive
in `go.mod`.
**This is the Go compatibility promise made machine-enforceable.**
### 5.3 GOEXPERIMENT: Compile-Time Feature Gates
```go
// In internal/goexperiment/flags.go:
// GOEXPERIMENT=jsonv2 enables the new JSON API
```
**Convention:** Major API additions ship behind experiment flags.
`encoding/json/v2` (6,387 lines, Apr 2025) exists in the tree but is
only visible when `GOEXPERIMENT=jsonv2` is set. This allows the code
to be developed in-tree, tested by adventurous users, and refined
before the compatibility promise applies.
### 5.4 Compiler Directives as Hidden Language
```go
//go:linkname localFunction remote/package.Function
//go:nosplit
//go:nowritebarrier
//go:noescape
//go:systemstack
```
**1,711 `go:linkname` directives** and **2,428 runtime compiler
directives** in non-test code. These are effectively a hidden language
within Go — they bypass the type system, calling conventions, and
garbage collector safety for performance-critical paths.
**Convention:** Directives are ONLY acceptable in the runtime and
compiler. The Go team is actively trying to reduce `go:linkname` usage
(92 touches in 2024 — many are removals). Third-party packages that use
`go:linkname` to access internals are explicitly unsupported.
### 5.5 Generated Code: The SSA Compiler
The SSA (Static Single Assignment) compiler backend is 417,686 lines,
but the largest files are generated:
```
opGen.go — 97,135 lines (generated)
rewriteAMD64.go — 79,703 lines (generated)
rewritegeneric.go — 38,337 lines (generated)
rewriteARM64.go — 26,203 lines (generated)
```
**Convention:** Generated files are checked into the repo (not
generated at build time). They contain a `// Code generated` header.
The generators live alongside their output. This means `git blame`
works on generated code — you can trace when a rewrite rule was added.
### 5.6 The Runtime: 129K Lines of Go + Assembly
```
proc.go — 8,156 lines (THE goroutine scheduler)
malloc.go — 2,501 lines (memory allocator)
mgc.go — 2,315 lines (garbage collector)
mheap.go — 3,030 lines (heap management)
panic.go — 1,788 lines (panic/recover machinery)
```
The scheduler (`proc.go`) documents its own design at the top:
> "The main concepts are: G - goroutine. M - worker thread, or machine.
> P - processor, a resource that is required to execute Go code."
**Convention:** The runtime combines Go and assembly (200 `.s` files).
Architecture-specific operations (context switches, atomic ops, system
calls) are in assembly. Everything else is in Go, using compiler
directives to bypass safety checks where needed.
---
## 6. PR Discussion Patterns
Go uses GitHub issues for proposals, not PRs for discussion. The key
governance mechanism is the **proposal process** with designated
reviewers.
### json/v2 (Issue #71497, 201 comments, Jan 2025)
"The largest major revision of a standard Go package to date."
**Key design decision:** Split into `encoding/json/v2` (semantic) and
`encoding/jsontext` (syntactic). The syntactic layer has no reflection
dependency — it's a pure JSON tokenizer.
**Ship strategy:** Land behind `GOEXPERIMENT=jsonv2`, iterate with
community feedback, then graduate. The code lives in-tree but doesn't
count as a compatibility commitment until the experiment flag is
removed.
**External validation:** Built as `github.com/go-json-experiment/json`
first, iterated for years, then proposed for stdlib. The implementation
preceded the proposal.
### GODEBUG (Issue #56986, 70 comments, Nov 2022)
Russ Cox's proposal for machine-enforced backward compatibility.
**The problem:** Sort algorithm changes, bug fixes, and behavior
improvements can break programs that depend on old behavior.
**The solution:** `go.mod`'s `go` directive becomes a compatibility
declaration. Programs built with Go 1.22 but declaring `go 1.20`
automatically get Go 1.20 behavior for any setting that changed between
1.20 and 1.22.
**Lesson:** The Go team solved "how do you improve a language without
breaking users" with a general mechanism rather than case-by-case
migration. Each new GODEBUG setting is a structured backward
compatibility opt-out.
### Generics (Issue #15292, 874 comments, 2016-2021)
5 years of discussion. The most debated language change in Go's history.
**Lesson:** Committee-driven projects can take years on foundational
decisions because consensus requires addressing every edge case. Compare
to Elixir's formatter (1 hour, zero comments) — the BDFL model moves
faster but accepts more single-point-of-failure risk.
### slog (Issue #56345, 841 comments, 2022-2023)
Structured logging took 10 months and 841 comments to land.
**Lesson:** Even when the need is clear and the solution is well-known,
Go's process requires exhaustive discussion. The result is usually
better (slog is well-designed), but the cost is measured in months.
---
## 7. Cross-Ecosystem Comparisons
| Aspect | Go | Elixir |
|--------|-----|--------|
| TODOs | 3,428, owner-attributed, permanent | 127, version-gated, deadlines |
| Self-hosting | Complete since 2015 | 33 Erlang files permanently |
| Feature gates | GOEXPERIMENT (compile-time) | None (ship or don't) |
| Compat mechanism | GODEBUG (79 settings) | Deprecation → removal on version |
| Governance | Committee (proposals, 874-comment threads) | BDFL (José, 1-hour merges) |
| Internal boundary | `internal/` (compiler-enforced, 61 packages) | OTP applications (convention-enforced) |
| Generated code | Checked in (97K-line files) | Compile-time (no artifacts) |
| Assembly | 200 .s files in runtime | None (delegates to Erlang/BEAM) |
| Biggest file | 97,135 lines (generated) | 7,102 lines (Kernel) |
---
## 8. What This Teaches
1. **`internal/` solves "shared but not public" at the language level.**
61 packages that other ecosystems have to solve with conventions,
Go solves with compiler enforcement. This is why Go projects
rarely have "utils" packages that leak abstraction — the pattern
exists in the language itself.
2. **GODEBUG is the most sophisticated backward compatibility mechanism
in any language runtime.** It makes the compatibility promise
*machine-verifiable* rather than *socially-enforced*. Programs don't
just get old behavior by default — they get it because their `go.mod`
declares what era they belong to.
3. **GOEXPERIMENT enables fearless iteration in the stdlib.** json/v2
can exist in-tree, be tested, be refined — all without triggering
the compatibility promise. This is "feature flags for a language."
4. **3,428 TODOs is honest, not sloppy.** Each one has an owner. They
document known limitations rather than hiding them. Go prefers
"explicitly imperfect" over "implicitly broken."
5. **Compiler directives are a hidden language.** 4,139 directives
(linkname + runtime pragmas) bypass Go's safety model. The Go team
accepts that the runtime needs a different language than users —
but actively restricts this power from escaping to third-party code.
6. **Generated code checked in > generated at build time** for
archaeology. `git blame` on `rewriteAMD64.go` tells you when a
codegen rule was added and why. Build-time generation loses this
history.
7. **Committee governance produces better designs but 10x slower.** Go's
slog (841 comments, 10 months) vs Elixir's formatter (0 comments,
1 hour). The designs are comparable in quality — the process cost
is where they differ.
8. **`unsafe` in Go's own source (1,304 imports) proves that safety
rules are for users, not for runtime implementors.** The people who
wrote the safety rules know exactly where they don't apply.
<!-- PATTERN_COMPLETE -->
-270
View File
@@ -1,270 +0,0 @@
# Go Language Source: Convention Reference
Quick-reference for conventions extracted from the golang/go source
code. Each entry: pattern name, location, example, when to use,
when NOT to use, origin.
---
## Owner-Attributed TODOs
**Location:** Throughout `src/`
```go
// TODO(gri): consider using a different approach here
// TODO(rsc): this should be cleaned up in the next release
```
**When to use:** Known limitations that a specific person should
address. The owner tag creates accountability without creating an
issue (issues are for user-visible problems; TODOs are for internal
engineering debt).
**When NOT to use:** User-visible bugs (file an issue instead).
Aspirational improvements with no clear owner. Anything that should
block a release.
**Origin:** Convention since Go's earliest commits. The Go team
averages 3,428 TODOs across the codebase — this is a conscious
engineering culture, not neglect.
---
## `internal/` Packages
**Location:** `src/internal/` (61 packages)
```go
// internal/singleflight — dedup concurrent calls
// internal/godebug — runtime feature flags
// internal/poll — OS I/O polling
// internal/bisect — binary search debugging
```
**When to use:** Code shared between stdlib packages that should NOT
become public API. Utility code that isn't stable enough for the
compatibility promise. Implementation details that users shouldn't
depend on.
**When NOT to use:** Code that external packages need. Code that's
stable enough for public API (promote it). One-off helpers that only
one package uses (keep them package-private).
**Origin:** `src/internal/` existed since Go moved sources from
`src/pkg` to `src` (2014). The compiler enforces the import restriction
— no code outside the tree can import internal packages.
---
## GODEBUG: Runtime Feature Flags
**Location:** `internal/godebug/godebug.go` (316 lines)
```go
var http2server = godebug.New("http2server")
func ServeConn(c net.Conn) {
if http2server.Value() == "0" {
// user opted out of HTTP/2
}
// IncNonDefault must be called each time non-default behavior fires
http2server.IncNonDefault()
}
```
**When to use:** Behavior changes that might break existing programs.
The old behavior becomes accessible via `GODEBUG=setting=value`. Tied
to `go.mod`'s `go` directive for automatic version-based defaults.
**When NOT to use:** Bug fixes that no reasonable program depends on.
New features (use GOEXPERIMENT instead). Performance optimizations that
don't change observable behavior.
**Origin:** Brad Fitzpatrick added the package in Aug 2021. Russ Cox
formalized it as the backward compatibility mechanism in proposal
#56986 (Nov 2022, 70 comments). 79 settings now exist.
---
## GOEXPERIMENT: Compile-Time Feature Gates
**Location:** `internal/goexperiment/flags.go` (136 lines)
```go
// Set via: GOEXPERIMENT=jsonv2 go build
// Check via build tag: //go:build goexperiment.jsonv2
```
**When to use:** Major new APIs or behavior changes that need real-world
testing before committing to the compatibility promise. The code lives
in-tree but isn't visible without the flag.
**When NOT to use:** Small features that can ship directly. Bug fixes.
Internal refactoring. Anything that doesn't need user feedback before
committing.
**Origin:** The GOEXPERIMENT mechanism predates its formalization — used
for fieldtrack, regabi, unified. json/v2 (Apr 2025) is the highest-
profile use: 6,387 lines shipped behind a flag.
---
## Compiler Directives
**Location:** Throughout `runtime/` and `cmd/`
```go
//go:linkname localName remote/pkg.ExportedName
//go:nosplit
//go:nowritebarrier
//go:noescape
//go:systemstack
```
**When to use:** ONLY in the runtime and compiler. `linkname` accesses
unexported symbols across packages. `nosplit` prevents stack growth
checks. `nowritebarrier` asserts no GC barriers. `systemstack` forces
execution on the system stack.
**When NOT to use:** In application code. In stdlib packages outside
runtime. In third-party packages (explicitly unsupported — the Go team
actively removes `go:linkname` targets that external packages depend
on).
**Origin:** 1,711 `go:linkname` and 2,428 other runtime directives.
The Go team is actively trying to reduce linkname usage (92 touches
in 2024, many removals).
---
## Generated Code (Checked In)
**Location:** `cmd/compile/internal/ssa/`
```go
// Code generated by ssa/gen/*.go; DO NOT EDIT.
// opGen.go — 97,135 lines
// rewriteAMD64.go — 79,703 lines
// rewritegeneric.go — 38,337 lines
```
**When to use:** When the generated output needs `git blame` history.
When generators should be auditable alongside their output. When build
reproducibility matters (no external tool dependency).
**When NOT to use:** When the generated output is large AND changes
frequently (diff noise). When the generator is trivial (just use
`go generate` at build time).
**Origin:** SSA compiler introduced on dev.ssa branch (2015). The
rewrite rules are generated from a DSL but checked in so reviewers
can see exactly what changed.
---
## Assembly in Runtime
**Location:** `runtime/*.s` (200 files)
```asm
// runtime/asm_amd64.s
TEXT runtime·gogo(SB), NOSPLIT, $0-8
MOVQ buf+0(FP), BX // gobuf
MOVQ gobuf_g(BX), DX
...
```
**When to use:** Context switches, atomic operations, system calls,
and anything that needs direct control over registers/stack. Each
architecture has its own set of assembly files.
**When NOT to use:** Anything that can be written in Go with acceptable
performance. The Go team actively converts assembly to Go where
possible (e.g., crypto packages moving from asm to Go+intrinsics).
**Origin:** Runtime has always included assembly — Go's scheduler and
GC require direct hardware control that no high-level language can
provide.
---
## Proposal Process (Committee Governance)
**Location:** GitHub issues, not PRs
```
Issue #71497 (json/v2): 201 comments, Jan 2025
Issue #56986 (GODEBUG): 70 comments, Nov 2022
Issue #56345 (slog): 841 comments, 10 months
Issue #15292 (generics): 874 comments, 5 years
```
**When to use:** Any user-visible API addition or behavior change.
The proposal process ensures all edge cases are considered before
committing to the compatibility promise.
**When NOT to use:** Internal refactoring, performance improvements
that don't change API, bug fixes. These go through normal code review
(Gerrit/GitHub).
**Origin:** The Go proposal process evolved from informal (early Go)
to formalized (2015+). Russ Cox and the Go team manage a proposal
review meeting that triages and decides.
---
## The Scheduler as Documentation
**Location:** `runtime/proc.go` (8,156 lines)
```go
// Goroutine scheduler
// The scheduler's job is to distribute ready-to-run goroutines over
// worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
```
**When to use:** Complex algorithms should document their model at the
top of the file. The Go runtime uses extensive comments to explain
the scheduler's invariants, the GC's phases, and the memory
allocator's structure.
**When NOT to use:** Simple code that's self-documenting. Comments
that restate what the code does rather than WHY it does it.
**Origin:** The G-M-P model comment dates to the scheduler rewrite
(2014). The convention of architectural documentation at file-top
extends throughout the runtime.
---
## json/v2: Experiment-to-Stdlib Pipeline
**Location:** `encoding/json/v2/` (6,387 lines, 13 files)
```go
// encoding/json/v2 — semantic layer (uses reflection)
// encoding/jsontext — syntactic layer (no reflection dependency)
```
**When to use:** When building a major stdlib revision. The pattern:
1. Build as external module (`go-json-experiment/json`)
2. Iterate with real users for years
3. Propose for stdlib with working implementation
4. Ship behind GOEXPERIMENT flag
5. Graduate when stable
**When NOT to use:** Small additions that don't need years of
iteration. Features that can be backward-compatible additions to
existing packages.
**Origin:** `go-json-experiment/json` existed since 2022. Proposed as
#71497 (Jan 2025, 201 comments). Landed behind flag Apr 2025 (commit
`0e17905793` by Damien Neil).
<!-- PATTERN_COMPLETE -->
-182
View File
@@ -1,182 +0,0 @@
# Patterns Extracted from prometheus/prometheus
## Pattern: Atomic File Operations with Suffix Convention
**Source:** `tsdb/db.go`
**Category:** storage
**What:** Use directory suffixes (`.tmp-for-creation`,
`.tmp-for-deletion`) to make multi-step file operations
crash-safe. On startup, clean up any dirs with these
suffixes (they represent incomplete operations).
**Why:** Database storage needs atomicity. If the process
crashes between creating a block and finalizing it, you
need to know the block is incomplete. The suffix convention
makes incomplete state visible at the filesystem level
without requiring a separate journal.
**Example:**
```go
const (
tmpForDeletionBlockDirSuffix = ".tmp-for-deletion"
tmpForCreationBlockDirSuffix = ".tmp-for-creation"
)
// On startup: remove any .tmp-* dirs (incomplete ops)
// On create: write to dir.tmp-for-creation, then rename
// On delete: rename to dir.tmp-for-deletion, then remove
```
**When to use:** Any system that manages files/directories
and needs crash consistency without a full WAL. Simpler
than a write-ahead log for coarse-grained operations.
**When NOT to use:** When you already have a WAL or
transaction log. Or for fine-grained operations where
rename semantics are insufficient.
---
## Pattern: DefaultOptions() Function
**Source:** `tsdb/db.go`
**Category:** configuration
**What:** Provide a `DefaultOptions()` function returning a
fully-populated config struct. Users copy and override only
what they need. No nil-means-default ambiguity.
**Why:** Large config structs (20+ fields) are unwieldy.
By providing sane defaults as a function (not a
package-level var), you avoid mutation bugs and make it
clear what "normal" looks like. Users only specify
deviations.
**Example:**
```go
func DefaultOptions() *Options {
return &Options{
WALSegmentSize: wlog.DefaultSegmentSize,
RetentionDuration: int64(15*24*time.Hour / ...),
MinBlockDuration: DefaultBlockDuration,
MaxBlockDuration: DefaultBlockDuration,
SamplesPerChunk: DefaultSamplesPerChunk,
// ... 20 more fields with sane defaults
}
}
// Usage:
opts := tsdb.DefaultOptions()
opts.RetentionDuration = 30 * 24 * time.Hour
db, err := tsdb.Open(dir, nil, nil, opts, nil)
```
**When to use:** Config structs with many fields where most
users want defaults. Especially when zero-value semantics
would be confusing (e.g., 0 retention = infinite? or off?).
**When NOT to use:** Small configs (3-4 fields) where
struct literal with zero-means-default is clear enough.
---
## Pattern: Scrape Loop with Aligned Timestamps
**Source:** `scrape/scrape.go`
**Category:** concurrency
**What:** Periodic scrape loops that align timestamps to
intervals with a small tolerance, enabling better storage
compression downstream.
**Why:** Time-series databases compress better when
timestamps are regular. A 2ms tolerance on alignment
means scraped data aligns to the expected grid while
accommodating real-world jitter.
**Example:**
```go
var ScrapeTimestampTolerance = 2 * time.Millisecond
var AlignScrapeTimestamps = true
// In scrape loop: if scrape finishes within tolerance
// of expected timestamp, snap to the grid
```
**When to use:** Any periodic data collection where
downstream storage benefits from timestamp regularity.
Metrics, heartbeats, polling loops.
**When NOT to use:** Event-driven data where timestamps
must reflect actual occurrence time. Audit logs, user
actions, financial transactions.
---
## Pattern: Sentinel Errors with Interface Check
**Source:** `tsdb/db.go`
**Category:** error-handling
**What:** Define package-level sentinel errors with
`errors.New()` and use compile-time interface assertions
to verify implementations satisfy storage interfaces.
**Why:** `ErrNotReady` as a sentinel lets callers use
`errors.Is` for retry logic. The pattern ensures error
identity is stable across versions (not string-matched).
**Example:**
```go
var ErrNotReady = errors.New("TSDB not ready")
// Callers can reliably detect this:
if errors.Is(err, tsdb.ErrNotReady) {
// Retry later — DB is still initializing
}
```
**When to use:** Any error that callers need to handle
programmatically (retry, fallback, special UI). Make it a
named sentinel, not a string comparison.
**When NOT to use:** Errors that are always terminal or
always logged-and-discarded. Not every error needs a name.
---
## Pattern: Compile-Time Interface Satisfaction
**Source:** `scrape/scrape.go`
**Category:** organization
**What:** Use `var _ Interface = (*Type)(nil)` to verify at
compile time that a type satisfies an interface, even if
the type is only used dynamically.
**Why:** Without this, you discover missing methods only
when the type is actually used — which might be in a
rarely-exercised code path or only in production. The
compile-time check catches it immediately.
**Example:**
```go
var _ FailureLogger = (*logging.JSONFileLogger)(nil)
// Fails at compile time if JSONFileLogger doesn't
// implement FailureLogger
```
**When to use:** Any type that implements an interface
consumed dynamically (registered in a map, stored as
interface value, passed to framework code).
**When NOT to use:** Types whose interface satisfaction is
already enforced by direct usage in the same package.
<!-- PATTERN_COMPLETE -->