Compare commits
34
Commits
b5d0544fd6
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
484dc7dd07 | ||
|
|
52a3629583 | ||
|
|
f070fef8ee | ||
|
|
befe49257a | ||
|
|
a8e2020bc4 | ||
|
|
be394efd0b | ||
|
|
65a433d0c6 | ||
|
|
0de5f54365 | ||
|
|
52503d24c2 | ||
|
|
7bcc1cc62b | ||
|
|
ffcc0fccf3 | ||
|
|
d73c81dab1 | ||
|
|
c8ed244a07 | ||
|
|
99c0865e93 | ||
|
|
c7e61565c0 | ||
|
|
dfe03e0675 | ||
|
|
65cae45f13 | ||
|
|
fe74d5d47c | ||
|
|
498a3e9b27 | ||
|
|
2f7536766c | ||
|
|
4185747da6 | ||
|
|
d6f36b67c8 | ||
|
|
cee58e85a4 | ||
|
|
725308c37a | ||
|
|
758ae5dae4 | ||
|
|
1ef2a4a189 | ||
|
|
733aa7d261 | ||
|
|
11048ae73e | ||
|
|
a7a853bb43 | ||
|
|
631be02392 | ||
|
|
eb9171368b | ||
|
|
0e5974f39a | ||
|
|
c797178fb9 | ||
|
|
0f1d7e4c06 |
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
# 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 -->
|
||||||
+451
-207
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
|
|||||||
|
|
||||||
## 1. sync.Mutex — The Basic Lock
|
## 1. sync.Mutex — The Basic Lock
|
||||||
|
|
||||||
### Source: `src/sync/mutex.go:18-34`, `src/sync/mutex.go:42-67`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/mutex.go:18-34
|
// [src/sync/mutex.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L18)
|
||||||
// A Mutex is a mutual exclusion lock.
|
// A Mutex is a mutual exclusion lock.
|
||||||
// The zero value for a Mutex is an unlocked mutex.
|
// The zero value for a Mutex is an unlocked mutex.
|
||||||
//
|
//
|
||||||
@@ -19,21 +19,16 @@ type Mutex struct {
|
|||||||
mu isync.Mutex
|
mu isync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/sync/mutex.go:36-39
|
// [src/sync/mutex.go#L36](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L36)
|
||||||
type Locker interface {
|
type Locker interface {
|
||||||
Lock()
|
Lock()
|
||||||
Unlock()
|
Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/sync/mutex.go:43-46
|
// [src/sync/mutex.go#L43](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L43)
|
||||||
func (m *Mutex) Lock() {
|
func (m *Mutex) Lock() {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/sync/mutex.go:64-67
|
|
||||||
func (m *Mutex) Unlock() {
|
|
||||||
m.mu.Unlock()
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
@@ -43,6 +38,85 @@ func (m *Mutex) Unlock() {
|
|||||||
- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock
|
- **Not associated with a goroutine** — one goroutine can Lock, another can Unlock
|
||||||
- **Locker interface** — abstracts over Mutex and RWMutex
|
- **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
|
### Idiomatic Usage
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -60,12 +134,11 @@ type Config struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
data map[string]string
|
data map[string]string
|
||||||
}
|
}
|
||||||
c2 := *c1 // COPIES the mutex — data race waiting to happen
|
c2 := *c1 // COPIES the mutex — data race
|
||||||
|
|
||||||
// DON'T: Forget defer
|
// DON'T: Forget defer
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
// if this panics, the mutex stays locked forever
|
doSomething() // if this panics, mutex stays locked forever
|
||||||
doSomething()
|
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -73,24 +146,24 @@ mu.Unlock()
|
|||||||
|
|
||||||
## 2. sync.Once — Exactly-Once Initialization
|
## 2. sync.Once — Exactly-Once Initialization
|
||||||
|
|
||||||
### Source: `src/sync/once.go:12-36`, `src/sync/once.go:56-79`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/once.go:12-23
|
// [src/sync/once.go#L12](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L12)
|
||||||
type Once struct {
|
type Once struct {
|
||||||
_ noCopy
|
_ noCopy
|
||||||
done atomic.Bool
|
done atomic.Bool
|
||||||
m Mutex
|
m Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/sync/once.go:56-63
|
// [src/sync/once.go#L56](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L56)
|
||||||
func (o *Once) Do(f func()) {
|
func (o *Once) Do(f func()) {
|
||||||
if !o.done.Load() {
|
if !o.done.Load() {
|
||||||
o.doSlow(f)
|
o.doSlow(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/sync/once.go:65-72
|
// [src/sync/once.go#L65](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/once.go#L65)
|
||||||
func (o *Once) doSlow(f func()) {
|
func (o *Once) doSlow(f func()) {
|
||||||
o.m.Lock()
|
o.m.Lock()
|
||||||
defer o.m.Unlock()
|
defer o.m.Unlock()
|
||||||
@@ -103,10 +176,101 @@ func (o *Once) doSlow(f func()) {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
The implementation reveals a subtle guarantee: **when Do returns, f has finished**. The naive CAS-only approach (documented in the comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait.
|
The implementation reveals a subtle guarantee: **when Do returns, f has finished**. The naive CAS-only approach (documented in comment at line 56-63) would let the second caller return before f completes. The mutex ensures all callers wait.
|
||||||
|
|
||||||
The `done` field is first in the struct for hot-path performance on amd64/386 (noted in comment at line 24-27).
|
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
|
### Idiomatic Usage
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -126,18 +290,6 @@ func GetDB() *DB {
|
|||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```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)
|
// DON'T: Call Do recursively (deadlocks)
|
||||||
var once sync.Once
|
var once sync.Once
|
||||||
once.Do(func() {
|
once.Do(func() {
|
||||||
@@ -149,16 +301,12 @@ once.Do(func() {
|
|||||||
|
|
||||||
## 3. sync.WaitGroup — Waiting for Goroutine Completion
|
## 3. sync.WaitGroup — Waiting for Goroutine Completion
|
||||||
|
|
||||||
### Source: `src/sync/waitgroup.go:14-43`, `src/sync/waitgroup.go:236-260`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/waitgroup.go:14-43
|
// [src/sync/waitgroup.go#L14](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L14)
|
||||||
// A WaitGroup is a counting semaphore typically used to wait
|
// Typically, a main goroutine will start tasks by calling WaitGroup.Go
|
||||||
// for a group of goroutines or tasks to finish.
|
// and then wait for all tasks to complete by calling WaitGroup.Wait:
|
||||||
//
|
|
||||||
// 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
|
// var wg sync.WaitGroup
|
||||||
// wg.Go(task1)
|
// wg.Go(task1)
|
||||||
@@ -174,13 +322,14 @@ type WaitGroup struct {
|
|||||||
### Go 1.25+: WaitGroup.Go
|
### Go 1.25+: WaitGroup.Go
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/waitgroup.go:236-260
|
// [src/sync/waitgroup.go#L236](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/waitgroup.go#L236)
|
||||||
func (wg *WaitGroup) Go(f func()) {
|
func (wg *WaitGroup) Go(f func()) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if x := recover(); x != nil {
|
if x := recover(); x != nil {
|
||||||
panic(x) // don't call Done — let panic propagate
|
// Don't call Done — let panic propagate fatally.
|
||||||
|
panic(x)
|
||||||
}
|
}
|
||||||
wg.Done()
|
wg.Done()
|
||||||
}()
|
}()
|
||||||
@@ -191,7 +340,7 @@ func (wg *WaitGroup) Go(f func()) {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
`WaitGroup.Go` (new in Go 1.25) encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit.
|
`WaitGroup.Go` encapsulates the Add/go/Done pattern. Key design: if `f` panics, it re-panics **without** calling Done, preventing the main goroutine from racing to exit before the panic stack trace prints.
|
||||||
|
|
||||||
### Classic Pattern (pre-Go 1.25)
|
### Classic Pattern (pre-Go 1.25)
|
||||||
|
|
||||||
@@ -219,33 +368,27 @@ for _, item := range items {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
wg.Wait()
|
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
|
## 4. sync.Pool — Object Reuse for GC Pressure
|
||||||
|
|
||||||
### Source: `src/sync/pool.go:44-63`
|
### Source: [src/sync/pool.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/pool.go:44-63
|
// [src/sync/pool.go#L44](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/pool.go#L44)
|
||||||
// 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,
|
// Pool's purpose is to cache allocated but unused items for later reuse,
|
||||||
// relieving pressure on the garbage collector.
|
// relieving pressure on the garbage collector. That is, it makes it easy to
|
||||||
|
// build efficient, thread-safe free lists.
|
||||||
|
//
|
||||||
|
// An appropriate use of a Pool is to manage a group of temporary items
|
||||||
|
// silently shared among and potentially reused by concurrent independent
|
||||||
|
// clients of a package. Pool provides a way to amortize allocation overhead
|
||||||
|
// across many clients.
|
||||||
|
//
|
||||||
|
// An example of good use of a Pool is in the fmt package, which maintains a
|
||||||
|
// dynamically-sized store of temporary output buffers.
|
||||||
type Pool struct {
|
type Pool struct {
|
||||||
noCopy noCopy
|
noCopy noCopy
|
||||||
local unsafe.Pointer
|
local unsafe.Pointer
|
||||||
@@ -258,7 +401,7 @@ type Pool struct {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
Pool is **not** a general cache. Items can vanish between GC cycles. It's for reducing allocation pressure on hot paths — `fmt` uses it for print buffers, `encoding/json` for encoder state.
|
Pool is **not** a general cache. Items can vanish between GC cycles. Use for reducing allocation pressure on hot paths.
|
||||||
|
|
||||||
### Idiomatic Usage (from fmt package)
|
### Idiomatic Usage (from fmt package)
|
||||||
|
|
||||||
@@ -286,33 +429,28 @@ func (p *pp) free() {
|
|||||||
var connPool = sync.Pool{
|
var connPool = sync.Pool{
|
||||||
New: func() any { return connectToDB() },
|
New: func() any { return connectToDB() },
|
||||||
}
|
}
|
||||||
// Connections may be GC'd at any time — use database/sql's pool instead
|
// Connections may be GC'd — use database/sql's pool instead
|
||||||
|
|
||||||
// DON'T: Put dirty objects back without resetting
|
// DON'T: Put dirty objects back without resetting
|
||||||
pool.Put(buf) // still has data from last use — memory leak or data leak
|
pool.Put(buf) // still has data from last use
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Channel as Done Signal (Context Pattern)
|
## 5. Channel as Done Signal (Context Pattern)
|
||||||
|
|
||||||
### Source: `src/context/context.go:83-100` (Done channel), `src/io/pipe.go:42-45`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/context/context.go:83-100
|
// [src/context/context.go#L83](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L83)
|
||||||
// Done returns a channel that's closed when work done on behalf of this
|
// Done returns a channel that's closed when work done on behalf of this
|
||||||
// context should be canceled.
|
// context should be canceled.
|
||||||
Done() <-chan struct{}
|
Done() <-chan struct{}
|
||||||
|
|
||||||
// src/io/pipe.go:42-45
|
// [src/io/pipe.go#L42](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L42)
|
||||||
type pipe struct {
|
type pipe struct {
|
||||||
wrMu sync.Mutex
|
|
||||||
wrCh chan []byte
|
|
||||||
rdCh chan int
|
|
||||||
once sync.Once
|
once sync.Once
|
||||||
done chan struct{} // closed on pipe close
|
done chan struct{} // closed on pipe close
|
||||||
rerr onceError
|
|
||||||
werr onceError
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -329,14 +467,113 @@ 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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Use chan bool for done signals
|
// DON'T: Use chan bool for done signals
|
||||||
done := make(chan bool) // wastes 1 byte per signal, true/false meaningless
|
done := make(chan bool) // wastes 1 byte, true/false meaningless
|
||||||
|
|
||||||
// DON'T: Send to done (only works once, only one receiver)
|
// DON'T: Send to done (only unblocks one receiver)
|
||||||
done <- struct{}{} // only unblocks one goroutine
|
done <- struct{}{}
|
||||||
|
|
||||||
// DO: Close the channel (broadcasts to all)
|
// DO: Close the channel (broadcasts to all)
|
||||||
close(done)
|
close(done)
|
||||||
@@ -344,15 +581,12 @@ close(done)
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Context Propagation
|
## 6. Context Propagation Rules
|
||||||
|
|
||||||
### Source: `src/context/context.go:37-48` (rules), `src/net/http/request.go:368-380`
|
### Source: [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
|
||||||
|
|
||||||
From the package doc:
|
|
||||||
```go
|
```go
|
||||||
// src/context/context.go:37-48
|
// [src/context/context.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L37)
|
||||||
// Programs that use Contexts should follow these rules:
|
|
||||||
//
|
|
||||||
// Do not store Contexts inside a struct type; instead, pass a Context
|
// 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
|
// explicitly to each function that needs it. The Context should be the first
|
||||||
// parameter, typically named ctx:
|
// parameter, typically named ctx:
|
||||||
@@ -365,34 +599,19 @@ From the package doc:
|
|||||||
// if you are unsure about which Context to use.
|
// 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
|
### Why
|
||||||
|
|
||||||
Context flows **down** the call chain, never stored in structs. `WithContext` returns a shallow copy — the original request is not mutated. This is the immutable-context pattern.
|
Context flows **down** the call chain, never stored in structs. It carries deadlines and cancellation signals for the current request, not persistent state.
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Store context in a struct
|
// DON'T: Store context in a struct
|
||||||
type Server struct {
|
type Server struct {
|
||||||
ctx context.Context // stale context persists beyond request lifecycle
|
ctx context.Context // stale context persists beyond request
|
||||||
}
|
}
|
||||||
|
|
||||||
// DON'T: Pass nil context
|
// DON'T: Pass nil
|
||||||
doWork(nil, data) // use context.TODO() if unsure
|
doWork(nil, data) // use context.TODO() if unsure
|
||||||
|
|
||||||
// DON'T: Put context anywhere other than first parameter
|
// DON'T: Put context anywhere other than first parameter
|
||||||
@@ -401,64 +620,65 @@ func doWork(data Data, ctx context.Context) // wrong position
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Context Cancellation (WithCancel/WithTimeout)
|
## 7. Context Cancellation with Timeout
|
||||||
|
|
||||||
### Source: `src/context/context.go:242-249` (WithCancel), `src/net/http/server.go:4007-4050` (TimeoutHandler)
|
### Source: [src/net/http/server.go#L4007](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4007) (TimeoutHandler)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/context/context.go:242-249
|
// [src/net/http/server.go#L4011](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L4011)
|
||||||
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) {
|
func (h *timeoutHandler) ServeHTTP(w ResponseWriter, r *Request) {
|
||||||
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
|
ctx, cancelCtx := context.WithTimeout(r.Context(), h.dt)
|
||||||
defer cancelCtx()
|
defer cancelCtx()
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
// ...
|
panicChan := make(chan any, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
panicChan <- p
|
||||||
|
}
|
||||||
|
}()
|
||||||
h.handler.ServeHTTP(tw, r)
|
h.handler.ServeHTTP(tw, r)
|
||||||
close(done)
|
close(done)
|
||||||
}()
|
}()
|
||||||
select {
|
select {
|
||||||
|
case p := <-panicChan:
|
||||||
|
panic(p)
|
||||||
case <-done:
|
case <-done:
|
||||||
// handler completed
|
// handler completed — copy response
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
// timeout
|
// timeout — write 503
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
`defer cancelCtx()` is critical — it releases resources (timers, goroutines) when the parent returns, even if the child hasn't timed out yet. The go vet tool checks for this.
|
This is the full pattern: context with timeout + goroutine + select on done/timeout/panic. Key details:
|
||||||
|
1. `defer cancelCtx()` — always release resources
|
||||||
|
2. Panic propagation via dedicated channel
|
||||||
|
3. Select on three outcomes: success, timeout, panic
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Forget to call cancel (leaks goroutines)
|
// DON'T: Forget to call cancel (leaks timer goroutines)
|
||||||
ctx, _ := context.WithCancel(parent) // cancel function discarded!
|
ctx, _ := context.WithTimeout(parent, 5*time.Second)
|
||||||
|
|
||||||
// DON'T: Cancel before work starts
|
// DON'T: Ignore context in long operations
|
||||||
ctx, cancel := context.WithTimeout(parent, 5*time.Second)
|
func longWork(ctx context.Context) {
|
||||||
cancel() // immediately cancels — no work can happen
|
time.Sleep(10 * time.Minute) // ignores cancellation
|
||||||
doWork(ctx)
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Select with Done Channel
|
## 8. Select with Non-Blocking Check
|
||||||
|
|
||||||
### Source: `src/context/context.go:83-100` (Done in select), `src/io/pipe.go:51-60`
|
### Source: [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/pipe.go:51-60
|
// [src/io/pipe.go#L51](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L51)
|
||||||
func (p *pipe) read(b []byte) (n int, err error) {
|
func (p *pipe) read(b []byte) (n int, err error) {
|
||||||
select {
|
select {
|
||||||
case <-p.done:
|
case <-p.done:
|
||||||
@@ -479,26 +699,7 @@ func (p *pipe) read(b []byte) (n int, err error) {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
The double-select pattern: first a non-blocking check (with `default`), then a blocking wait. The non-blocking check prevents a race where `done` was closed between the last operation and the current one.
|
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.
|
||||||
|
|
||||||
### 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
|
### Anti-pattern
|
||||||
|
|
||||||
@@ -510,71 +711,29 @@ for {
|
|||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
// busy-spins CPU at 100%
|
// busy-spins CPU at 100%!
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Goroutine-per-Connection (net/http Server)
|
## 9. Channel Pipeline (io.Pipe)
|
||||||
|
|
||||||
### Source: `src/net/http/server.go` (conceptual — the serve loop spawns goroutines per connection)
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// The pattern (simplified from server.go serve loop):
|
// [src/io/pipe.go#L38](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L38)
|
||||||
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 {
|
type pipe struct {
|
||||||
wrMu sync.Mutex
|
|
||||||
wrCh chan []byte // writer sends data slices
|
wrCh chan []byte // writer sends data slices
|
||||||
rdCh chan int // reader returns bytes consumed
|
rdCh chan int // reader returns bytes consumed
|
||||||
once sync.Once
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
rerr onceError
|
|
||||||
werr onceError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/pipe.go:195-205
|
// [src/io/pipe.go#L195](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/pipe.go#L195)
|
||||||
func Pipe() (*PipeReader, *PipeWriter) {
|
func Pipe() (*PipeReader, *PipeWriter) {
|
||||||
pw := &PipeWriter{r: PipeReader{pipe: pipe{
|
pw := &PipeWriter{r: PipeReader{pipe: pipe{
|
||||||
wrCh: make(chan []byte),
|
wrCh: make(chan []byte), // unbuffered
|
||||||
rdCh: make(chan int),
|
rdCh: make(chan int), // unbuffered
|
||||||
done: make(chan struct{}),
|
done: make(chan struct{}),
|
||||||
}}}
|
}}}
|
||||||
return &pw.r, pw
|
return &pw.r, pw
|
||||||
@@ -583,9 +742,9 @@ func Pipe() (*PipeReader, *PipeWriter) {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
`io.Pipe` connects a Writer to a Reader using **unbuffered channels** — each Write blocks until the corresponding Read consumes the data. No internal buffering means backpressure is automatic. The `done` channel signals when either end closes.
|
`io.Pipe` uses **unbuffered channels** — each Write blocks until Read consumes. Backpressure is automatic. The `done` channel signals shutdown.
|
||||||
|
|
||||||
### Pattern: Channel Pipeline
|
### Pipeline Pattern Template
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func generate(ctx context.Context) <-chan int {
|
func generate(ctx context.Context) <-chan int {
|
||||||
@@ -604,6 +763,91 @@ 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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -614,7 +858,7 @@ func produce() <-chan int {
|
|||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
ch <- i
|
ch <- i
|
||||||
}
|
}
|
||||||
// forgot close(ch) — receivers hang on range
|
// forgot close(ch) — range receivers hang
|
||||||
}()
|
}()
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
@@ -622,18 +866,17 @@ func produce() <-chan int {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. database/sql Connection Opener Goroutine
|
## 10. Background Worker with Context Shutdown
|
||||||
|
|
||||||
### Source: `src/database/sql/sql.go:836-843`
|
### Source: [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/database/sql/sql.go:836-843
|
// [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
|
||||||
func OpenDB(c driver.Connector) *DB {
|
func OpenDB(c driver.Connector) *DB {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
db := &DB{
|
db := &DB{
|
||||||
connector: c,
|
connector: c,
|
||||||
openerCh: make(chan struct{}, connectionRequestQueueSize),
|
openerCh: make(chan struct{}, connectionRequestQueueSize),
|
||||||
lastPut: make(map[*driverConn]string),
|
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
}
|
}
|
||||||
go db.connectionOpener(ctx)
|
go db.connectionOpener(ctx)
|
||||||
@@ -643,27 +886,27 @@ func OpenDB(c driver.Connector) *DB {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
A dedicated background goroutine (`connectionOpener`) processes connection requests from a buffered channel. The goroutine is controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down cleanly. This is the "long-lived worker goroutine with context shutdown" pattern.
|
A dedicated background goroutine processes work from a buffered channel. It's controlled by a context — calling `cancel()` (stored as `db.stop`) shuts it down. This is the standard "long-lived worker goroutine with graceful shutdown" pattern.
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Start background goroutines without shutdown mechanism
|
// DON'T: Start goroutines without shutdown mechanism
|
||||||
go func() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
processWork() // runs forever, no way to stop it
|
processWork() // runs forever, no way to stop
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 12. noCopy — Preventing Value Copies at Vet Time
|
## 11. noCopy — Preventing Value Copies
|
||||||
|
|
||||||
### Source: `src/sync/cond.go:120-126`
|
### Source: [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/cond.go:120-126
|
// [src/sync/cond.go#L120](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/cond.go#L120)
|
||||||
type noCopy struct{}
|
type noCopy struct{}
|
||||||
|
|
||||||
// Lock is a no-op used by -copylocks checker from `go vet`.
|
// Lock is a no-op used by -copylocks checker from `go vet`.
|
||||||
@@ -673,14 +916,14 @@ func (*noCopy) Unlock() {}
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
Embedding `noCopy` in a struct makes `go vet` report an error when the struct is copied. All sync primitives use this because copying a locked mutex or in-use WaitGroup is always a bug.
|
Embedding `noCopy` makes `go vet` report errors when the struct is copied by value. All sync primitives use this because copying a locked mutex or active WaitGroup is always a bug.
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Pass sync types by value
|
// DON'T: Pass sync types by value
|
||||||
func doWork(wg sync.WaitGroup) { // copies the WaitGroup!
|
func doWork(wg sync.WaitGroup) { // copies!
|
||||||
defer wg.Done()
|
defer wg.Done() // operates on copy, not original
|
||||||
}
|
}
|
||||||
|
|
||||||
// DO: Pass by pointer
|
// DO: Pass by pointer
|
||||||
@@ -702,6 +945,7 @@ func doWork(wg *sync.WaitGroup) {
|
|||||||
| Signal completion/cancellation | `chan struct{}` + `close()` |
|
| Signal completion/cancellation | `chan struct{}` + `close()` |
|
||||||
| Deadline/timeout propagation | `context.WithTimeout` / `context.WithCancel` |
|
| Deadline/timeout propagation | `context.WithTimeout` / `context.WithCancel` |
|
||||||
| Backpressure between producer/consumer | Unbuffered channels |
|
| Backpressure between producer/consumer | Unbuffered channels |
|
||||||
| Fan-out with results | Buffered channel + WaitGroup |
|
|
||||||
| Long-lived background worker | Goroutine + context cancellation |
|
| Long-lived background worker | Goroutine + context cancellation |
|
||||||
| Prevent struct copying | Embed `noCopy` field |
|
| Prevent struct copying | Embed `noCopy` field |
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
|
|||||||
@@ -0,0 +1,989 @@
|
|||||||
|
# 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 -->
|
||||||
@@ -0,0 +1,460 @@
|
|||||||
|
# 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 -->
|
||||||
+265
-54
@@ -6,21 +6,21 @@ Patterns extracted from the Go standard library source code.
|
|||||||
|
|
||||||
## 1. Sentinel Errors
|
## 1. Sentinel Errors
|
||||||
|
|
||||||
### Source: `src/io/io.go:40-43` (EOF), `src/errors/errors.go:81-83` (ErrUnsupported)
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:40-43
|
// [src/io/io.go#L40](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L40)
|
||||||
// EOF is the error returned by Read when no more input is available.
|
// EOF is the error returned by Read when no more input is available.
|
||||||
// (Read must return EOF itself, not an error wrapping EOF,
|
// (Read must return EOF itself, not an error wrapping EOF,
|
||||||
// because callers will test for EOF using ==.)
|
// because callers will test for EOF using ==.)
|
||||||
var EOF = errors.New("EOF")
|
var EOF = errors.New("EOF")
|
||||||
|
|
||||||
// src/io/io.go:47-49
|
// [src/io/io.go#L47](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L47)
|
||||||
var ErrUnexpectedEOF = errors.New("unexpected EOF")
|
var ErrUnexpectedEOF = errors.New("unexpected EOF")
|
||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/errors.go:81-83
|
// [src/errors/errors.go#L81](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L81)
|
||||||
var ErrUnsupported = New("unsupported operation")
|
var ErrUnsupported = New("unsupported operation")
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,6 +36,86 @@ 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).
|
**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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -52,15 +132,15 @@ func Read() error {
|
|||||||
|
|
||||||
## 2. errors.New — Minimal Error Construction
|
## 2. errors.New — Minimal Error Construction
|
||||||
|
|
||||||
### Source: `src/errors/errors.go:62-69`
|
### Source: [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/errors.go:62-64
|
// [src/errors/errors.go#L62](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L62)
|
||||||
func New(text string) error {
|
func New(text string) error {
|
||||||
return &errorString{text}
|
return &errorString{text}
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/errors/errors.go:66-69
|
// [src/errors/errors.go#L66](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L66)
|
||||||
type errorString struct {
|
type errorString struct {
|
||||||
s string
|
s string
|
||||||
}
|
}
|
||||||
@@ -97,10 +177,10 @@ func doThing() error {
|
|||||||
|
|
||||||
## 3. Error Wrapping with fmt.Errorf and %w
|
## 3. Error Wrapping with fmt.Errorf and %w
|
||||||
|
|
||||||
### Source: `src/fmt/errors.go:13-23`, `src/fmt/errors.go:70-80`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/fmt/errors.go:13-23
|
// [src/fmt/errors.go#L13](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L13)
|
||||||
// Errorf formats according to a format specifier and returns the string
|
// Errorf formats according to a format specifier and returns the string
|
||||||
// as a value that satisfies error.
|
// as a value that satisfies error.
|
||||||
//
|
//
|
||||||
@@ -110,7 +190,7 @@ func doThing() error {
|
|||||||
// Unwrap method returning a []error containing all the %w operands.
|
// Unwrap method returning a []error containing all the %w operands.
|
||||||
func Errorf(format string, a ...any) (err error) { ... }
|
func Errorf(format string, a ...any) (err error) { ... }
|
||||||
|
|
||||||
// src/fmt/errors.go:70-80
|
// [src/fmt/errors.go#L70](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/errors.go#L70)
|
||||||
type wrapError struct {
|
type wrapError struct {
|
||||||
msg string
|
msg string
|
||||||
err error
|
err error
|
||||||
@@ -137,11 +217,85 @@ return fmt.Errorf("open config: %w", err)
|
|||||||
return fmt.Errorf("open config: %v", 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
|
### When to use %w vs %v
|
||||||
|
|
||||||
- **%w**: When the wrapped error is part of your API contract. Callers can depend on it.
|
- **%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.
|
- **%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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -156,10 +310,10 @@ return fmt.Errorf("internal: %w", internalErr) // now callers depend on interna
|
|||||||
|
|
||||||
## 4. errors.Is — Checking Error Identity Through Chains
|
## 4. errors.Is — Checking Error Identity Through Chains
|
||||||
|
|
||||||
### Source: `src/errors/wrap.go:30-44`
|
### Source: [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/wrap.go:30-44
|
// [src/errors/wrap.go#L30](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L30)
|
||||||
func Is(err, target error) bool {
|
func Is(err, target error) bool {
|
||||||
if err == nil || target == nil {
|
if err == nil || target == nil {
|
||||||
return err == target
|
return err == target
|
||||||
@@ -223,10 +377,10 @@ if errors.Is(err, os.ErrNotExist) { ... } // works through wrapping
|
|||||||
|
|
||||||
## 5. errors.As — Extracting Error Types Through Chains
|
## 5. errors.As — Extracting Error Types Through Chains
|
||||||
|
|
||||||
### Source: `src/errors/wrap.go:96-120`
|
### Source: [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/wrap.go:96-120
|
// [src/errors/wrap.go#L96](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/wrap.go#L96)
|
||||||
func As(err error, target any) bool {
|
func As(err error, target any) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
@@ -252,7 +406,7 @@ if errors.As(err, &pathErr) {
|
|||||||
|
|
||||||
### Go 1.24+: errors.AsType (generic version)
|
### Go 1.24+: errors.AsType (generic version)
|
||||||
|
|
||||||
From `src/errors/errors.go:48-56` doc:
|
From [src/errors/errors.go#L48](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L48) doc:
|
||||||
```go
|
```go
|
||||||
if perr, ok := errors.AsType[*fs.PathError](err); ok {
|
if perr, ok := errors.AsType[*fs.PathError](err); ok {
|
||||||
fmt.Println(perr.Path)
|
fmt.Println(perr.Path)
|
||||||
@@ -274,10 +428,10 @@ if errors.As(err, &pathErr) { ... } // works through wrapping
|
|||||||
|
|
||||||
## 6. errors.Join — Multi-Error Aggregation
|
## 6. errors.Join — Multi-Error Aggregation
|
||||||
|
|
||||||
### Source: `src/errors/join.go:20-39`
|
### Source: [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/join.go:20-39
|
// [src/errors/join.go#L20](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/join.go#L20)
|
||||||
func Join(errs ...error) error {
|
func Join(errs ...error) error {
|
||||||
n := 0
|
n := 0
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
@@ -300,17 +454,11 @@ func Join(errs ...error) error {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `joinError` type implements `Unwrap() []error`:
|
The `joinError` type implements `Unwrap() []error`, making both `Is` and `As` traverse correctly.
|
||||||
```go
|
|
||||||
// src/errors/join.go:57 (implicit from structure)
|
|
||||||
func (e *joinError) Unwrap() []error {
|
|
||||||
return e.errs
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error. Both `Is` and `As` traverse the tree correctly.
|
For operations that can produce multiple errors (closing multiple resources, validating multiple fields), `Join` collects them into a single error.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
var errs []error
|
var errs []error
|
||||||
@@ -319,6 +467,77 @@ errs = append(errs, closeCache())
|
|||||||
return errors.Join(errs...) // nil if all nil
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -336,7 +555,7 @@ return lastErr
|
|||||||
|
|
||||||
## 7. Custom Is() Method — Equivalence Classes
|
## 7. Custom Is() Method — Equivalence Classes
|
||||||
|
|
||||||
### Source: `src/errors/wrap.go:42-44` (doc comment), `src/context/context.go:177-179`
|
### 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)
|
||||||
|
|
||||||
From the `errors.Is` doc:
|
From the `errors.Is` doc:
|
||||||
```go
|
```go
|
||||||
@@ -350,7 +569,7 @@ From the `errors.Is` doc:
|
|||||||
|
|
||||||
Real example from context:
|
Real example from context:
|
||||||
```go
|
```go
|
||||||
// src/context/context.go:177-179
|
// [src/context/context.go#L177](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L177)
|
||||||
type deadlineExceededError struct{}
|
type deadlineExceededError struct{}
|
||||||
|
|
||||||
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
|
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
|
||||||
@@ -375,10 +594,10 @@ func (e MyError) Is(target error) bool {
|
|||||||
|
|
||||||
## 8. Error Wrapping in Custom Types (Unwrap pattern)
|
## 8. Error Wrapping in Custom Types (Unwrap pattern)
|
||||||
|
|
||||||
### Source: `src/encoding/json/encode.go:276-293`
|
### Source: [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/encoding/json/encode.go:276-282
|
// [src/encoding/json/encode.go#L276](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/encode.go#L276)
|
||||||
type MarshalerError struct {
|
type MarshalerError struct {
|
||||||
Type reflect.Type
|
Type reflect.Type
|
||||||
Err error
|
Err error
|
||||||
@@ -431,13 +650,12 @@ func (e *MyError) Error() string { return e.Err.Error() }
|
|||||||
|
|
||||||
## 9. ErrUnsupported — Feature Detection via Errors
|
## 9. ErrUnsupported — Feature Detection via Errors
|
||||||
|
|
||||||
### Source: `src/errors/errors.go:76-83`
|
### Source: [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/errors/errors.go:76-83
|
// [src/errors/errors.go#L76](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/errors/errors.go#L76)
|
||||||
// ErrUnsupported indicates that a requested operation cannot be performed,
|
// ErrUnsupported indicates that a requested operation cannot be performed,
|
||||||
// because it is unsupported. For example, a call to os.Link when using a
|
// because it is unsupported.
|
||||||
// file system that does not support hard links.
|
|
||||||
//
|
//
|
||||||
// Functions and methods should not return this error but should instead
|
// Functions and methods should not return this error but should instead
|
||||||
// return an error including appropriate context that satisfies
|
// return an error including appropriate context that satisfies
|
||||||
@@ -456,8 +674,6 @@ This pattern separates "what happened" (detailed context) from "what kind of fai
|
|||||||
return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported)
|
return fmt.Errorf("chmod %s: %w", path, errors.ErrUnsupported)
|
||||||
```
|
```
|
||||||
|
|
||||||
Callers check the sentinel; the message provides context.
|
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -467,46 +683,39 @@ return errors.ErrUnsupported // no info about what operation or why
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Error Value Patterns in net/http
|
## 10. Error String Conventions
|
||||||
|
|
||||||
### Source: `src/net/http/server.go:39-56`
|
### Source: [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go:39-56
|
// [src/net/http/server.go#L39](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L39)
|
||||||
var (
|
var (
|
||||||
ErrBodyNotAllowed = internal.ErrBodyNotAllowed
|
|
||||||
ErrHijacked = errors.New("http: connection has been hijacked")
|
ErrHijacked = errors.New("http: connection has been hijacked")
|
||||||
ErrContentLength = errors.New("http: wrote more than the declared Content-Length")
|
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
|
### Convention: Error String Format
|
||||||
|
|
||||||
```
|
```
|
||||||
package: description
|
package: description
|
||||||
```
|
```
|
||||||
|
|
||||||
Examples from stdlib:
|
- Lowercase (no capital first letter)
|
||||||
- `"http: connection has been hijacked"`
|
- No trailing punctuation
|
||||||
- `"sql: unknown driver %q (forgotten import?)"`
|
- Package prefix for disambiguation
|
||||||
- `"json: unsupported type: %s"`
|
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Capitalize error strings
|
// DON'T: Capitalize error strings
|
||||||
errors.New("Connection has been hijacked") // Go convention: lowercase
|
errors.New("Connection has been hijacked")
|
||||||
|
|
||||||
// DON'T: End error strings with punctuation
|
// DON'T: End with punctuation
|
||||||
errors.New("connection failed.") // no trailing period
|
errors.New("connection failed.")
|
||||||
|
|
||||||
|
// DON'T: Include redundant "error" word
|
||||||
|
errors.New("http error: connection failed") // it's already an error
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -533,3 +742,5 @@ Is this a specific, well-known condition?
|
|||||||
| Aggregate multiple errors | `errors.Join(err1, err2)` |
|
| Aggregate multiple errors | `errors.Join(err1, err2)` |
|
||||||
| Make custom types traversable | Implement `Unwrap() error` |
|
| Make custom types traversable | Implement `Unwrap() error` |
|
||||||
| Define error equivalence | Implement `Is(error) bool` |
|
| Define error equivalence | Implement `Is(error) bool` |
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
|
|||||||
+318
-28
@@ -6,22 +6,22 @@ Patterns extracted from the Go standard library source code.
|
|||||||
|
|
||||||
## 1. Small Interfaces (1-2 Methods)
|
## 1. Small Interfaces (1-2 Methods)
|
||||||
|
|
||||||
### Source: `src/io/io.go:80-92` (Reader), `93-103` (Writer), `105-109` (Closer)
|
### 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)
|
||||||
|
|
||||||
Go's most powerful interfaces have exactly **one method**:
|
Go's most powerful interfaces have exactly **one method**:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:80-92
|
// [src/io/io.go#L80](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L80)
|
||||||
type Reader interface {
|
type Reader interface {
|
||||||
Read(p []byte) (n int, err error)
|
Read(p []byte) (n int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:93-103
|
// [src/io/io.go#L93](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L93)
|
||||||
type Writer interface {
|
type Writer interface {
|
||||||
Write(p []byte) (n int, err error)
|
Write(p []byte) (n int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:105-109
|
// [src/io/io.go#L105](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L105)
|
||||||
type Closer interface {
|
type Closer interface {
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,74 @@ 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`.
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -52,30 +120,30 @@ Large interfaces are hard to implement, hard to mock, and couple consumers to ca
|
|||||||
|
|
||||||
## 2. Interface Composition
|
## 2. Interface Composition
|
||||||
|
|
||||||
### Source: `src/io/io.go:131-155`
|
### Source: [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
|
||||||
|
|
||||||
Compose small interfaces into larger ones only when needed:
|
Compose small interfaces into larger ones only when needed:
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:131-134
|
// [src/io/io.go#L131](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L131)
|
||||||
type ReadWriter interface {
|
type ReadWriter interface {
|
||||||
Reader
|
Reader
|
||||||
Writer
|
Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:136-139
|
// [src/io/io.go#L136](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L136)
|
||||||
type ReadCloser interface {
|
type ReadCloser interface {
|
||||||
Reader
|
Reader
|
||||||
Closer
|
Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:141-144
|
// [src/io/io.go#L141](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L141)
|
||||||
type WriteCloser interface {
|
type WriteCloser interface {
|
||||||
Writer
|
Writer
|
||||||
Closer
|
Closer
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:146-150
|
// [src/io/io.go#L146](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L146)
|
||||||
type ReadWriteCloser interface {
|
type ReadWriteCloser interface {
|
||||||
Reader
|
Reader
|
||||||
Writer
|
Writer
|
||||||
@@ -100,13 +168,13 @@ func processData(rw ReadWriteCloser) {
|
|||||||
|
|
||||||
## 3. Accept Interfaces, Return Structs
|
## 3. Accept Interfaces, Return Structs
|
||||||
|
|
||||||
### Source: `src/io/io.go:461` (LimitReader), `src/io/io.go:618` (TeeReader)
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:461
|
// src/io/io.go:461
|
||||||
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
|
func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }
|
||||||
|
|
||||||
// src/io/io.go:467-471
|
// [src/io/io.go#L467](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L467)
|
||||||
type LimitedReader struct {
|
type LimitedReader struct {
|
||||||
R Reader // underlying reader
|
R Reader // underlying reader
|
||||||
N int64 // max bytes remaining
|
N int64 // max bytes remaining
|
||||||
@@ -114,7 +182,7 @@ type LimitedReader struct {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:618-620
|
// [src/io/io.go#L618](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L618)
|
||||||
func TeeReader(r Reader, w Writer) Reader {
|
func TeeReader(r Reader, w Writer) Reader {
|
||||||
return &teeReader{r, w}
|
return &teeReader{r, w}
|
||||||
}
|
}
|
||||||
@@ -127,6 +195,69 @@ 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).
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -141,7 +272,7 @@ func NewServer() ServerInterface // hides useful config fields
|
|||||||
|
|
||||||
## 4. Interface Satisfaction as a Compile-Time Check
|
## 4. Interface Satisfaction as a Compile-Time Check
|
||||||
|
|
||||||
### Source: `src/io/io.go:645`, `src/net/http/server.go:4071`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:645
|
// src/io/io.go:645
|
||||||
@@ -168,10 +299,10 @@ func doSomething(w ResponseWriter) {
|
|||||||
|
|
||||||
## 5. Interface-Based Polymorphism (sort.Interface)
|
## 5. Interface-Based Polymorphism (sort.Interface)
|
||||||
|
|
||||||
### Source: `src/sort/sort.go:16-41`
|
### Source: [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sort/sort.go:16-41
|
// [src/sort/sort.go#L16](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sort/sort.go#L16)
|
||||||
type Interface interface {
|
type Interface interface {
|
||||||
Len() int
|
Len() int
|
||||||
Less(i, j int) bool
|
Less(i, j int) bool
|
||||||
@@ -197,17 +328,17 @@ Note: Since Go 1.21, `slices.SortFunc` is preferred for slices (generic + faster
|
|||||||
|
|
||||||
## 6. The Adapter Pattern (HandlerFunc)
|
## 6. The Adapter Pattern (HandlerFunc)
|
||||||
|
|
||||||
### Source: `src/net/http/server.go:2334-2342`
|
### Source: [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go:2334-2338
|
// [src/net/http/server.go#L2334](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2334)
|
||||||
// The HandlerFunc type is an adapter to allow the use of
|
// The HandlerFunc type is an adapter to allow the use of
|
||||||
// ordinary functions as HTTP handlers. If f is a function
|
// ordinary functions as HTTP handlers. If f is a function
|
||||||
// with the appropriate signature, HandlerFunc(f) is a
|
// with the appropriate signature, HandlerFunc(f) is a
|
||||||
// Handler that calls f.
|
// Handler that calls f.
|
||||||
type HandlerFunc func(ResponseWriter, *Request)
|
type HandlerFunc func(ResponseWriter, *Request)
|
||||||
|
|
||||||
// src/net/http/server.go:2341-2342
|
// [src/net/http/server.go#L2341](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L2341)
|
||||||
// ServeHTTP calls f(w, r).
|
// ServeHTTP calls f(w, r).
|
||||||
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
|
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
|
||||||
f(w, r)
|
f(w, r)
|
||||||
@@ -218,6 +349,78 @@ 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.
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -233,15 +436,15 @@ func (h myHandler) ServeHTTP(w ResponseWriter, r *Request) {
|
|||||||
|
|
||||||
## 7. Optional Interfaces (Runtime Feature Detection)
|
## 7. Optional Interfaces (Runtime Feature Detection)
|
||||||
|
|
||||||
### Source: `src/net/http/server.go:165-175` (Flusher), `src/net/http/server.go:183-206` (Hijacker)
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go:165-170
|
// [src/net/http/server.go#L165](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L165)
|
||||||
type Flusher interface {
|
type Flusher interface {
|
||||||
Flush()
|
Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/net/http/server.go:183-206
|
// [src/net/http/server.go#L183](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L183)
|
||||||
type Hijacker interface {
|
type Hijacker interface {
|
||||||
Hijack() (net.Conn, *bufio.ReadWriter, error)
|
Hijack() (net.Conn, *bufio.ReadWriter, error)
|
||||||
}
|
}
|
||||||
@@ -259,6 +462,91 @@ 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.
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -276,10 +564,10 @@ type ResponseWriter interface {
|
|||||||
|
|
||||||
## 8. The Stringer Interface (Convention-Based Behavior)
|
## 8. The Stringer Interface (Convention-Based Behavior)
|
||||||
|
|
||||||
### Source: `src/fmt/print.go:63-66`
|
### Source: [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/fmt/print.go:63-66
|
// [src/fmt/print.go#L63](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/fmt/print.go#L63)
|
||||||
type Stringer interface {
|
type Stringer interface {
|
||||||
String() string
|
String() string
|
||||||
}
|
}
|
||||||
@@ -308,10 +596,10 @@ func printThing(v any) string {
|
|||||||
|
|
||||||
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
|
## 9. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy)
|
||||||
|
|
||||||
### Source: `src/io/io.go:410-417`
|
### Source: [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:410-417
|
// [src/io/io.go#L410](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L410)
|
||||||
func copyBuffer(dst Writer, src Reader, buf []byte) (written int64, err error) {
|
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.
|
// If the reader has a WriteTo method, use it to do the copy.
|
||||||
// Avoids an allocation and a copy.
|
// Avoids an allocation and a copy.
|
||||||
@@ -344,15 +632,15 @@ func Copy(dst Writer, src Reader) {
|
|||||||
|
|
||||||
## 10. The driver.Driver Pattern (Plugin Interfaces)
|
## 10. The driver.Driver Pattern (Plugin Interfaces)
|
||||||
|
|
||||||
### Source: `src/database/sql/driver/driver.go:85-97`, `104-112`
|
### Source: [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85), `104-112`
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/database/sql/driver/driver.go:85-97
|
// [src/database/sql/driver/driver.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L85)
|
||||||
type Driver interface {
|
type Driver interface {
|
||||||
Open(name string) (Conn, error)
|
Open(name string) (Conn, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/database/sql/driver/driver.go:104-112
|
// [src/database/sql/driver/driver.go#L104](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/driver/driver.go#L104)
|
||||||
type DriverContext interface {
|
type DriverContext interface {
|
||||||
OpenConnector(name string) (Connector, error)
|
OpenConnector(name string) (Connector, error)
|
||||||
}
|
}
|
||||||
@@ -388,3 +676,5 @@ type Driver interface {
|
|||||||
| Compile-time interface checks | `var _ Interface = (*Type)(nil)` |
|
| Compile-time interface checks | `var _ Interface = (*Type)(nil)` |
|
||||||
| Runtime interface upgrade for optimization | `io.Copy` → `WriterTo`/`ReaderFrom` |
|
| Runtime interface upgrade for optimization | `io.Copy` → `WriterTo`/`ReaderFrom` |
|
||||||
| Plugin/driver interfaces start minimal | `database/sql/driver.Driver` |
|
| Plugin/driver interfaces start minimal | `database/sql/driver.Driver` |
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
|
|||||||
+277
-179
@@ -6,10 +6,10 @@ Patterns extracted from the Go standard library source code.
|
|||||||
|
|
||||||
## 1. Package-Level Documentation
|
## 1. Package-Level Documentation
|
||||||
|
|
||||||
### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go:5-13
|
// [src/io/io.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/io/io.go#L5)
|
||||||
// Package io provides basic interfaces to I/O primitives.
|
// Package io provides basic interfaces to I/O primitives.
|
||||||
// Its primary job is to wrap existing implementations of such primitives,
|
// Its primary job is to wrap existing implementations of such primitives,
|
||||||
// such as those in package os, into shared public interfaces that
|
// such as those in package os, into shared public interfaces that
|
||||||
@@ -22,7 +22,7 @@ package io
|
|||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/sync/mutex.go:5-11
|
// [src/sync/mutex.go#L5](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/sync/mutex.go#L5)
|
||||||
// Package sync provides basic synchronization primitives such as mutual
|
// Package sync provides basic synchronization primitives such as mutual
|
||||||
// exclusion locks. Other than the Once and WaitGroup types, most are intended
|
// exclusion locks. Other than the Once and WaitGroup types, most are intended
|
||||||
// for use by low-level library routines. Higher-level synchronization is
|
// for use by low-level library routines. Higher-level synchronization is
|
||||||
@@ -43,7 +43,6 @@ The package comment:
|
|||||||
### Convention
|
### Convention
|
||||||
|
|
||||||
- First sentence: `"Package X does Y."` or `"Package X provides Y."`
|
- 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
|
- For multi-file packages, put the package comment in `doc.go` or the primary file
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
@@ -55,10 +54,6 @@ package myutil
|
|||||||
// DON'T: Restate the obvious
|
// DON'T: Restate the obvious
|
||||||
// Package http provides HTTP stuff.
|
// Package http provides HTTP stuff.
|
||||||
package http
|
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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -74,16 +69,10 @@ package auth
|
|||||||
- `net/http` — not `net/httpserver`
|
- `net/http` — not `net/httpserver`
|
||||||
- `encoding/json` — not `encoding/jsonparser`
|
- `encoding/json` — not `encoding/jsonparser`
|
||||||
- `context` — not `ctx` or `contexts`
|
- `context` — not `ctx` or `contexts`
|
||||||
- `errors` — not `errs` or `errorhandling`
|
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
Go package names are:
|
Go package names are **short, lowercase, no underscores or mixedCaps**. The package name is part of every qualified identifier:
|
||||||
- **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
|
```go
|
||||||
// Good: package name provides context
|
// Good: package name provides context
|
||||||
@@ -95,18 +84,14 @@ context.Context // the type IS the context
|
|||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Stutter (repeat package name in exported identifiers)
|
// DON'T: Stutter
|
||||||
package http
|
package http
|
||||||
type HTTPServer struct{} // http.HTTPServer — redundant
|
type HTTPServer struct{} // http.HTTPServer — redundant
|
||||||
func NewHTTPClient() // http.NewHTTPClient — say "http" twice
|
|
||||||
|
|
||||||
// DON'T: Use utility/helper package names
|
// DON'T: Utility package names
|
||||||
package utils // what does it DO?
|
package utils // what does it DO?
|
||||||
package helpers // grab bag, no cohesion
|
package helpers // grab bag, no cohesion
|
||||||
package common // everything ends up here
|
package common // everything ends up here
|
||||||
|
|
||||||
// DON'T: Use plural when singular works
|
|
||||||
package requests // should be: package request
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -119,64 +104,97 @@ package requests // should be: package request
|
|||||||
src/net/http/internal/
|
src/net/http/internal/
|
||||||
├── ascii/
|
├── ascii/
|
||||||
├── chunked.go
|
├── chunked.go
|
||||||
├── common.go
|
|
||||||
├── http2/
|
├── http2/
|
||||||
├── httpcommon/
|
├── httpcommon/
|
||||||
├── httpsfv/
|
|
||||||
├── sniff.go
|
├── sniff.go
|
||||||
└── testcert/
|
└── testcert/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. For example:
|
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. This lets you share code between sub-packages without making it public API.
|
||||||
- `net/http/internal/ascii` can be imported by `net/http` and `net/http/...`
|
|
||||||
- It **cannot** be imported by `net/url` or any other package
|
|
||||||
|
|
||||||
This lets you share code between sub-packages without making it part of the public API.
|
- `net/http/internal/ascii` → importable by `net/http` and children
|
||||||
|
- NOT importable by `net/url` or any other package
|
||||||
|
|
||||||
### Usage Guidelines
|
### When to Use
|
||||||
|
|
||||||
|
**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!
|
||||||
```
|
```
|
||||||
myproject/
|
|
||||||
├── internal/ # shared across the project, but not importable externally
|
**Example — after:**
|
||||||
│ ├── auth/
|
```go
|
||||||
│ └── metrics/
|
// pkg/mylib/internal/parse/parse.go
|
||||||
├── cmd/
|
package parse
|
||||||
│ └── server/
|
|
||||||
└── pkg/ # actually public API (if you use this convention)
|
func InternalFormat(s string) Thing { ... } // only importable by pkg/mylib and children
|
||||||
└── client/
|
|
||||||
|
// pkg/mylib/sub/handler.go
|
||||||
|
import "pkg/mylib/internal/parse" // ✓ allowed
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Export implementation details that should be internal
|
// DON'T: Export implementation details
|
||||||
package mylib
|
package mylib
|
||||||
func HelperThatOnlyIUse() {} // pollutes API surface
|
func HelperThatOnlyIUse() {} // pollutes API surface
|
||||||
|
|
||||||
// DON'T: Put everything in internal/ (nothing is reusable)
|
// DO: Move to internal/
|
||||||
// Balance: internal/ for implementation; exported packages for contracts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Export Rules — The Capital Letter Boundary
|
## 4. Export Rules — The Capital Letter Boundary
|
||||||
|
|
||||||
### Source: Throughout stdlib — the convention is the language itself
|
### Source: `src/io/io.go` — exported vs unexported
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/io/io.go
|
// src/io/io.go
|
||||||
var EOF = errors.New("EOF") // exported: uppercase
|
var EOF = errors.New("EOF") // exported: uppercase
|
||||||
var errInvalidWrite = errors.New(...) // unexported: lowercase
|
var errInvalidWrite = errors.New(...) // unexported: lowercase
|
||||||
|
|
||||||
// src/io/io.go:622-625
|
|
||||||
type teeReader struct { // unexported type
|
type teeReader struct { // unexported type
|
||||||
r Reader
|
r Reader
|
||||||
w Writer
|
w Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/io/io.go:618
|
|
||||||
func TeeReader(r Reader, w Writer) Reader { // exported constructor
|
func TeeReader(r Reader, w Writer) Reader { // exported constructor
|
||||||
return &teeReader{r, w}
|
return &teeReader{r, w}
|
||||||
}
|
}
|
||||||
@@ -184,42 +202,27 @@ func TeeReader(r Reader, w Writer) Reader { // exported constructor
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
The exported/unexported boundary is Go's encapsulation mechanism. `teeReader` is unexported because:
|
`teeReader` is unexported because:
|
||||||
1. Users don't need to know its implementation
|
1. Users don't need to know its implementation
|
||||||
2. The return type is `Reader` (the interface) — maximum flexibility
|
2. The return type is `Reader` (interface) — maximum flexibility
|
||||||
3. The struct's fields can change without breaking anyone
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Export everything "just in case"
|
// DON'T: Export everything "just in case"
|
||||||
type Parser struct {
|
type Parser struct {
|
||||||
Input string // should this be settable? probably not
|
Input string // should this be settable?
|
||||||
buffer []byte // internal state — definitely not
|
buffer []byte // internal state
|
||||||
pos int
|
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
|
## 5. init() Functions — Use Sparingly
|
||||||
|
|
||||||
### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31`
|
### Source: [src/net/http/http2.go#L37](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/http2.go#L37)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/http2.go:37
|
// src/net/http/http2.go:37
|
||||||
@@ -230,18 +233,86 @@ func init() {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
`init()` runs automatically at program start, in dependency order. The stdlib uses it for:
|
The stdlib uses `init()` for:
|
||||||
- **Driver registration** (database drivers register via init)
|
- **Driver registration** (database drivers register via init)
|
||||||
- **Protocol negotiation** (HTTP/2 registers its handler)
|
- **Protocol negotiation** (HTTP/2 registers its handler)
|
||||||
- **Configuration from build tags** (`servemux121.go` — compatibility shim)
|
|
||||||
|
|
||||||
### Rules
|
### Rules
|
||||||
|
|
||||||
1. `init()` should have no side effects beyond registration
|
1. Should have no side effects beyond registration
|
||||||
2. No errors should be possible (can't return error from init)
|
2. No errors possible (can't return error from init)
|
||||||
3. Keep them short — they block program startup
|
3. Keep them short
|
||||||
4. Prefer explicit initialization in `main()` when possible
|
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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
@@ -251,16 +322,11 @@ func init() {
|
|||||||
cache = loadGigabyteFile() // blocks startup
|
cache = loadGigabyteFile() // blocks startup
|
||||||
}
|
}
|
||||||
|
|
||||||
// DON'T: Use init for configuration
|
// DO: Prefer explicit setup in main()
|
||||||
func init() {
|
|
||||||
port = os.Getenv("PORT") // harder to test, implicit dependency
|
|
||||||
}
|
|
||||||
|
|
||||||
// DO: Prefer explicit setup
|
|
||||||
func main() {
|
func main() {
|
||||||
db, err := connectToDatabase()
|
db, err := connectToDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err) // clear failure point
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -269,12 +335,10 @@ func main() {
|
|||||||
|
|
||||||
## 6. Functional Options Pattern
|
## 6. Functional Options Pattern
|
||||||
|
|
||||||
### Source: Not directly in stdlib, but `net/http.Server` and `database/sql.DB` demonstrate the problem it solves
|
The stdlib uses struct-based configuration (`http.Server`, `tls.Config`). The functional options pattern emerged from the community for APIs with many optional parameters:
|
||||||
|
|
||||||
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
|
```go
|
||||||
// The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney):
|
// The pattern (idiom from Rob Pike/Dave Cheney):
|
||||||
type Option func(*Server)
|
type Option func(*Server)
|
||||||
|
|
||||||
func WithTimeout(d time.Duration) Option {
|
func WithTimeout(d time.Duration) Option {
|
||||||
@@ -283,12 +347,6 @@ 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 {
|
func NewServer(addr string, opts ...Option) *Server {
|
||||||
s := &Server{addr: addr, timeout: 30 * time.Second}
|
s := &Server{addr: addr, timeout: 30 * time.Second}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -298,10 +356,10 @@ func NewServer(addr string, opts ...Option) *Server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### What the stdlib uses instead: Config structs
|
### What stdlib uses: Config structs
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go (Server struct acts as config)
|
// net/http — struct literal configuration
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: ":8080",
|
Addr: ":8080",
|
||||||
ReadTimeout: 5 * time.Second,
|
ReadTimeout: 5 * time.Second,
|
||||||
@@ -314,40 +372,27 @@ srv := &http.Server{
|
|||||||
|
|
||||||
| Approach | When |
|
| Approach | When |
|
||||||
|----------|------|
|
|----------|------|
|
||||||
| Config struct | Few options, all are data (stdlib preference) |
|
| Config struct | Few options, all data (stdlib preference) |
|
||||||
| Functional options | Many options, some involve behavior, public API stability matters |
|
| Functional options | Many options, some involve behavior, public API stability |
|
||||||
| 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
|
## 7. Constructor Pattern — NewX Functions
|
||||||
|
|
||||||
### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go:2639
|
// src/net/http/server.go:2638
|
||||||
func NewServeMux() *ServeMux {
|
func NewServeMux() *ServeMux {
|
||||||
return new(ServeMux)
|
return &ServeMux{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/database/sql/sql.go:836-843
|
// [src/database/sql/sql.go#L836](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/database/sql/sql.go#L836)
|
||||||
func OpenDB(c driver.Connector) *DB {
|
func OpenDB(c driver.Connector) *DB {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
db := &DB{
|
db := &DB{
|
||||||
connector: c,
|
connector: c,
|
||||||
openerCh: make(chan struct{}, connectionRequestQueueSize),
|
openerCh: make(chan struct{}, connectionRequestQueueSize),
|
||||||
lastPut: make(map[*driverConn]string),
|
|
||||||
stop: cancel,
|
stop: cancel,
|
||||||
}
|
}
|
||||||
go db.connectionOpener(ctx)
|
go db.connectionOpener(ctx)
|
||||||
@@ -357,24 +402,19 @@ func OpenDB(c driver.Connector) *DB {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
- `NewX()` when construction is trivial (just allocate)
|
- `NewX()` when construction is trivial
|
||||||
- `OpenX()` or `NewXWithConfig()` when construction involves resources, validation, or can fail
|
- `OpenX()` when construction involves resources or can fail
|
||||||
- Return `*T` (pointer to concrete type), not an interface
|
- Return `*T` (concrete), not an interface
|
||||||
|
- Zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`)
|
||||||
The zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`), making constructors unnecessary.
|
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Constructor that returns interface (hides useful methods)
|
// DON'T: Constructor that returns interface
|
||||||
func NewWriter() io.Writer { return &myWriter{} }
|
func NewWriter() io.Writer { return &myWriter{} } // hides methods
|
||||||
|
|
||||||
// DON'T: Require constructor when zero value works
|
// DON'T: Require constructor when zero value works
|
||||||
type Buffer struct {
|
// var b bytes.Buffer ← just works
|
||||||
buf []byte
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
// var b bytes.Buffer ← just works, no New needed
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -390,54 +430,42 @@ src/
|
|||||||
├── net/ # network primitives
|
├── net/ # network primitives
|
||||||
│ ├── http/ # HTTP protocol
|
│ ├── http/ # HTTP protocol
|
||||||
│ └── url/ # URL parsing
|
│ └── url/ # URL parsing
|
||||||
├── encoding/ # encoding interfaces
|
├── encoding/
|
||||||
│ ├── json/ # JSON codec
|
│ ├── json/ # JSON codec
|
||||||
│ └── xml/ # XML codec
|
│ └── xml/ # XML codec
|
||||||
├── database/
|
├── database/
|
||||||
│ └── sql/ # SQL database abstraction
|
│ └── sql/ # SQL abstraction
|
||||||
│ └── driver/ # SPI for database drivers
|
│ └── driver/ # SPI for drivers
|
||||||
└── context/ # cancellation propagation
|
└── context/ # cancellation propagation
|
||||||
```
|
```
|
||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
Each package has a single, clear responsibility:
|
Each package has a single, clear responsibility. Packages communicate through interfaces, not shared state.
|
||||||
- `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
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Package per type
|
// DON'T: Package per type (50 packages with 1 file each)
|
||||||
package user // just has User struct
|
package user
|
||||||
package order // just has Order struct
|
package order
|
||||||
package payment // just has Payment struct
|
package payment
|
||||||
// 50 packages with 1 file each — Go prefers fewer, larger packages
|
|
||||||
|
|
||||||
// DON'T: Circular dependencies
|
// DON'T: Circular dependencies
|
||||||
package a imports package b
|
package a imports package b
|
||||||
package b imports package a // compile error
|
package b imports package a // compile error
|
||||||
|
|
||||||
// FIX: Extract shared types into a third package, or merge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. API Design — database/sql Separation of Concerns
|
## 9. API Layering — User vs Implementor (database/sql)
|
||||||
|
|
||||||
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
|
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
|
||||||
|
|
||||||
Two distinct APIs in one subsystem:
|
|
||||||
|
|
||||||
**User-facing (database/sql):**
|
**User-facing (database/sql):**
|
||||||
```go
|
```go
|
||||||
db, _ := sql.Open("postgres", connStr)
|
db, _ := sql.Open("postgres", connStr)
|
||||||
rows, _ := db.QueryContext(ctx, "SELECT ...")
|
rows, _ := db.QueryContext(ctx, "SELECT ...")
|
||||||
defer rows.Close()
|
|
||||||
for rows.Next() {
|
|
||||||
rows.Scan(&id, &name)
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Driver-facing (database/sql/driver):**
|
**Driver-facing (database/sql/driver):**
|
||||||
@@ -454,41 +482,19 @@ type Conn interface {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic. Clean separation:
|
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.
|
||||||
- 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
|
## 10. Context Key Pattern — Type-Safe Context Values
|
||||||
|
|
||||||
### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252`
|
### 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)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/context/context.go:132-164 (from doc comment)
|
// [src/context/context.go#L132](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/context/context.go#L132) (from doc)
|
||||||
// Package user defines a User type that's stored in Contexts.
|
|
||||||
// package user
|
// package user
|
||||||
//
|
//
|
||||||
// import "context"
|
|
||||||
//
|
|
||||||
// type key int
|
// type key int
|
||||||
//
|
|
||||||
// var userKey key
|
// var userKey key
|
||||||
//
|
//
|
||||||
// func NewContext(ctx context.Context, u *User) context.Context {
|
// func NewContext(ctx context.Context, u *User) context.Context {
|
||||||
@@ -502,7 +508,7 @@ type Database interface {
|
|||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// src/net/http/server.go:244-252
|
// [src/net/http/server.go#L244](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/server.go#L244)
|
||||||
var (
|
var (
|
||||||
ServerContextKey = &contextKey{"http-server"}
|
ServerContextKey = &contextKey{"http-server"}
|
||||||
LocalAddrContextKey = &contextKey{"local-addr"}
|
LocalAddrContextKey = &contextKey{"local-addr"}
|
||||||
@@ -515,37 +521,129 @@ type contextKey struct {
|
|||||||
|
|
||||||
### Why
|
### Why
|
||||||
|
|
||||||
- **Unexported key type** prevents other packages from accessing or overwriting your values
|
- **Unexported key type** prevents other packages from accessing your values
|
||||||
- **Type-safe accessors** (`FromContext`) avoid type assertions at every call site
|
- **Type-safe accessors** avoid repeated type assertions
|
||||||
- **Pointer-based keys** (`&contextKey{...}`) guarantee uniqueness even with same string names
|
- **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.
|
||||||
|
|
||||||
### Anti-pattern
|
### Anti-pattern
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// DON'T: Use string keys (any package can collide)
|
// DON'T: Use string keys (collision risk)
|
||||||
ctx = context.WithValue(ctx, "user", user)
|
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
|
// DON'T: Store optional parameters in context
|
||||||
ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
|
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
|
## Summary: Package Design Principles
|
||||||
|
|
||||||
| Principle | Rule |
|
| Principle | Rule |
|
||||||
|-----------|------|
|
|-----------|------|
|
||||||
| Package comment | `"Package X does Y."` before `package` keyword |
|
| Package comment | `"Package X does Y."` before `package` keyword |
|
||||||
| Naming | Short, lowercase, no stutter (`http.Server` not `http.HTTPServer`) |
|
| Naming | Short, lowercase, no stutter |
|
||||||
| Encapsulation | `internal/` for shared-but-private code |
|
| Encapsulation | `internal/` for private shared code |
|
||||||
| Exports | Minimum viable surface; unexported by default |
|
| Exports | Minimum surface; unexported by default |
|
||||||
| init() | Only for registration; keep trivial |
|
| init() | Only for registration; prefer explicit setup |
|
||||||
| Constructors | `NewX()` → `*T`; prefer usable zero values |
|
| Constructors | `NewX()` → `*T`; prefer usable zero values |
|
||||||
| Organization | One concern per package; no circular deps |
|
| Organization | One concern per package |
|
||||||
| API layers | Separate user-facing from implementor-facing (SPI) |
|
| API layers | Separate user from implementor (SPI) |
|
||||||
| Context values | Unexported key type + typed accessors |
|
| Context values | Unexported key type + typed accessors |
|
||||||
| Configuration | Struct literals (stdlib) or functional options (community) |
|
| Configuration | Struct literals or functional options |
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
|
|||||||
@@ -0,0 +1,601 @@
|
|||||||
|
# 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 -->
|
||||||
@@ -0,0 +1,647 @@
|
|||||||
|
# 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 -->
|
||||||
+264
-14
@@ -1,5 +1,7 @@
|
|||||||
# Advanced Go Testing Patterns
|
# 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.
|
Patterns extracted from the Go standard library (`src/net/http/`, `src/encoding/json/`, `src/testing/`) and Kubernetes source code.
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -10,12 +12,104 @@ The canonical Go test style. Every Go stdlib test file uses this pattern.
|
|||||||
|
|
||||||
### Pattern Name: Anonymous Struct Test Table
|
### Pattern Name: Anonymous Struct Test Table
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/header_test.go` lines 17-108
|
**Source:** [src/net/http/header_test.go#L17](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/header_test.go#L17)
|
||||||
|
|
||||||
**What they do:** Define test cases as a slice of anonymous structs, iterate with a range loop.
|
**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.
|
**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.
|
**Anti-pattern:** Writing individual assertions for each case, or copy-pasting test functions that differ by one input.
|
||||||
|
|
||||||
**Code example (stdlib):**
|
**Code example (stdlib):**
|
||||||
@@ -53,7 +147,7 @@ func TestHeaderWrite(t *testing.T) {
|
|||||||
|
|
||||||
### Pattern Name: Named Table Tests with t.Run (Subtests)
|
### Pattern Name: Named Table Tests with t.Run (Subtests)
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/encoding/json/encode_test.go` lines 285-320, `/tmp/go-src/src/encoding/json/scanner_test.go` lines 30-50
|
**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)
|
||||||
|
|
||||||
**What they do:** Combine table-driven tests with `t.Run` for named subtests. Use a `CaseName` struct that captures file/line for error reporting.
|
**What they do:** Combine table-driven tests with `t.Run` for named subtests. Use a `CaseName` struct that captures file/line for error reporting.
|
||||||
|
|
||||||
@@ -88,7 +182,7 @@ func TestValid(t *testing.T) {
|
|||||||
|
|
||||||
### Pattern Name: CaseName with Caller Position Tracking
|
### Pattern Name: CaseName with Caller Position Tracking
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/encoding/json/internal/jsontest/testcase.go` lines 18-37
|
**Source:** [src/encoding/json/internal/jsontest/testcase.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/internal/jsontest/testcase.go#L18)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -122,7 +216,7 @@ func (pos CasePos) String() string {
|
|||||||
|
|
||||||
### Pattern Name: t.Helper() for Clean Stack Traces
|
### Pattern Name: t.Helper() for Clean Stack Traces
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/testing/testing.go` lines 1415-1435
|
**Source:** [src/testing/testing.go#L1415](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/testing/testing.go#L1415)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -162,7 +256,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
|
### Pattern Name: *testing.T as First Argument to Helpers
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 4555-4580
|
**Source:** [src/net/http/serve_test.go#L4555](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L4555)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -196,7 +290,7 @@ mustGet := func(url string, headers ...string) {
|
|||||||
|
|
||||||
### Pattern Name: t.Cleanup for Test-Scoped Resources
|
### Pattern Name: t.Cleanup for Test-Scoped Resources
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/testing/testing.go` lines 1439-1468, `/tmp/go-src/src/net/http/clientserver_test.go` lines 120-127
|
**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)
|
||||||
|
|
||||||
**What they do:** Use `t.Cleanup(fn)` instead of `defer` for resource cleanup in tests.
|
**What they do:** Use `t.Cleanup(fn)` instead of `defer` for resource cleanup in tests.
|
||||||
|
|
||||||
@@ -256,7 +350,7 @@ ServeFile(w, r, "testdata/file")
|
|||||||
|
|
||||||
### Pattern Name: Golden Files with -update Flag
|
### Pattern Name: Golden Files with -update Flag
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/cmd/gofmt/gofmt_test.go` lines 18, 113-138
|
**Source:** [src/cmd/gofmt/gofmt_test.go#L18](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/cmd/gofmt/gofmt_test.go#L18), 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.
|
**What they do:** Compare test output against `.golden` files. Provide a `-update` flag that regenerates golden files from current output when behavior intentionally changes.
|
||||||
|
|
||||||
@@ -266,6 +360,89 @@ ServeFile(w, r, "testdata/file")
|
|||||||
3. Golden files serve as documentation of expected behavior.
|
3. Golden files serve as documentation of expected behavior.
|
||||||
4. Reviewers can see exactly what output changed in diffs.
|
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.
|
**Anti-pattern:** Comparing against inline expected strings that span 50+ lines, or manually constructing expected output.
|
||||||
|
|
||||||
**Code example (stdlib):**
|
**Code example (stdlib):**
|
||||||
@@ -313,12 +490,83 @@ func TestRewrite(t *testing.T) {
|
|||||||
|
|
||||||
### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers
|
### Pattern Name: httptest.NewRecorder for Unit-Testing Handlers
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 387-393
|
**Source:** [src/net/http/serve_test.go#L387](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/serve_test.go#L387)
|
||||||
|
|
||||||
**What they do:** Use `httptest.NewRecorder()` to test HTTP handlers without starting a server. Captures status code, headers, and body.
|
**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.
|
**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.
|
**Anti-pattern:** Spinning up a full server to test handler logic that doesn't need networking.
|
||||||
|
|
||||||
**Code example (stdlib):**
|
**Code example (stdlib):**
|
||||||
@@ -345,7 +593,7 @@ func TestServeMuxHandler(t *testing.T) {
|
|||||||
|
|
||||||
### Pattern Name: httptest.NewServer for Integration-Style Tests
|
### Pattern Name: httptest.NewServer for Integration-Style Tests
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 203-280
|
**Source:** [src/net/http/clientserver_test.go#L203](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L203)
|
||||||
|
|
||||||
**What they do:** Use `httptest.NewServer` / `httptest.NewUnstartedServer` for end-to-end HTTP testing with a real TCP listener on localhost.
|
**What they do:** Use `httptest.NewServer` / `httptest.NewUnstartedServer` for end-to-end HTTP testing with a real TCP listener on localhost.
|
||||||
|
|
||||||
@@ -376,7 +624,7 @@ func newClientServerTest(t testing.TB, mode testMode, h Handler, opts ...any) *c
|
|||||||
|
|
||||||
### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes
|
### Pattern Name: b.ReportAllocs + b.RunParallel + b.SetBytes
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/encoding/json/bench_test.go` lines 85-101
|
**Source:** [src/encoding/json/bench_test.go#L85](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/encoding/json/bench_test.go#L85)
|
||||||
|
|
||||||
**What they do:** Combine `b.ReportAllocs()` for allocation reporting, `b.RunParallel` for concurrent benchmarks, and `b.SetBytes` for throughput metrics.
|
**What they do:** Combine `b.ReportAllocs()` for allocation reporting, `b.RunParallel` for concurrent benchmarks, and `b.SetBytes` for throughput metrics.
|
||||||
|
|
||||||
@@ -414,7 +662,7 @@ func BenchmarkCodeEncoder(b *testing.B) {
|
|||||||
|
|
||||||
### Pattern Name: testing.Short() for Expensive Tests
|
### Pattern Name: testing.Short() for Expensive Tests
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/serve_test.go` lines 800, 1000, 2212, 2581
|
**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
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -508,7 +756,7 @@ func afterTest(t testing.TB) {
|
|||||||
|
|
||||||
### Pattern Name: Bridge File for Internal Testing
|
### Pattern Name: Bridge File for Internal Testing
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/export_test.go` lines 1-50
|
**Source:** [src/net/http/export_test.go#L1](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/export_test.go#L1)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -533,7 +781,7 @@ var (
|
|||||||
|
|
||||||
### Pattern Name: Generic Test Runner Across Protocol Modes
|
### Pattern Name: Generic Test Runner Across Protocol Modes
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 100-134
|
**Source:** [src/net/http/clientserver_test.go#L100](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L100)
|
||||||
|
|
||||||
**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.
|
**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.
|
||||||
|
|
||||||
@@ -564,7 +812,7 @@ func run[T TBRun[T]](t T, f func(t T, mode testMode), opts ...any) {
|
|||||||
|
|
||||||
### Pattern Name: io.Writer Adapter for *testing.T
|
### Pattern Name: io.Writer Adapter for *testing.T
|
||||||
|
|
||||||
**Source:** `/tmp/go-src/src/net/http/clientserver_test.go` lines 337-345
|
**Source:** [src/net/http/clientserver_test.go#L337](https://github.com/golang/go/blob/17bd5ab8c650155dd2bd09f7005726552639eea0/src/net/http/clientserver_test.go#L337)
|
||||||
|
|
||||||
**What they do:** Implement `io.Writer` backed by `t.Logf`, so server error logs appear in test output (visible with `-v`, suppressed otherwise).
|
**What they do:** Implement `io.Writer` backed by `t.Logf`, so server error logs appear in test output (visible with `-v`, suppressed otherwise).
|
||||||
|
|
||||||
@@ -584,3 +832,5 @@ func (w testLogWriter) Write(b []byte) (int, error) {
|
|||||||
// Usage:
|
// Usage:
|
||||||
cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0)
|
cst.ts.Config.ErrorLog = log.New(testLogWriter{t}, "", 0)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<!-- PATTERN_COMPLETE -->
|
||||||
|
|||||||
@@ -1,670 +0,0 @@
|
|||||||
# 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"` |
|
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
# 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 -->
|
||||||
+530
-442
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# 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 -->
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
# 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 -->
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# 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 -->
|
||||||
Reference in New Issue
Block a user