Files
patterns-vs-guidelines/go/divergence-analysis.md

186 lines
9.0 KiB
Markdown

# Go Patterns vs Official Guidelines — Divergence Analysis
## Summary
- **Areas of agreement:** 12 (core patterns align with official guidance)
- **Divergences found:** 4 (our patterns make different emphasis or recommendations)
- **Our patterns go beyond official:** 8 (patterns we extracted that guides don't cover)
---
## Agreements (brief)
| Area | Official Source | Status |
|------|---------------|--------|
| Small interfaces (1-2 methods) | CodeReviewComments | Aligned |
| Accept interfaces, return structs | CodeReviewComments | Aligned |
| Don't define interfaces "for mocking" | CodeReviewComments | Aligned |
| Error strings: lowercase, no punctuation | CodeReviewComments | Aligned |
| Handle errors, don't discard with `_` | CodeReviewComments | Aligned |
| Indent error flow (happy path left) | CodeReviewComments | Aligned |
| Receiver names: short, consistent | Google Style Decisions | Aligned |
| MixedCaps / no underscores | Effective Go | Aligned |
| Acronyms all-caps (URL, ID, HTTP) | CodeReviewComments | Aligned |
| gofmt is non-negotiable | All guides | Aligned |
| Context as first param, never in structs | CodeReviewComments | Aligned |
| Don't Panic for normal errors | CodeReviewComments | Aligned |
---
## Divergences
### 1. Interface Definition Timing
**Our pattern says:** Design interfaces upfront — 10 patterns covering composition, adapters, optional interfaces.
**Official guide says:** "Do not define interfaces before they are used."
**Example — what our patterns teach:**
```go
// Our patterns show you this as a "good interface":
type Store interface {
Get(ctx context.Context, key string) ([]byte, error)
Put(ctx context.Context, key string, value []byte) error
}
```
**Example — what the official guide warns against:**
```go
// DON'T do this before you have 2+ implementations:
type UserService interface {
CreateUser(ctx context.Context, u User) error
GetUser(ctx context.Context, id string) (User, error)
UpdateUser(ctx context.Context, u User) error
DeleteUser(ctx context.Context, id string) error
}
// You only have one implementation. This interface is premature.
```
**Hypothesis:** Our patterns were extracted from *mature* stdlib code where interfaces already crystallized after years of use. The official guide targets the *development process* — advising you not to pre-design what you haven't needed yet. The stdlib had those needs. Our patterns describe the *outcome* of good design; the official guide guards the *process*.
**Who's right:** Official guide for new code (wait until you need it). Our patterns as reference for *what good looks like* once the design crystallizes.
---
### 2. Functional Options vs Config Structs
**Our pattern says:** Both valid — functional options for "many options, some involve behavior, public API stability."
**Official guide says:** Nothing about functional options. Stdlib universally uses config structs. Google Style's "least mechanism" implies: use the simpler thing.
**Example — what the stdlib does (and official guides implicitly endorse):**
```go
// Config struct with nil-means-default:
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
// Handler: nil means DefaultServeMux
}
```
**Example — what community/our patterns also endorse:**
```go
// Functional options:
db := postgres.Open(connStr,
postgres.WithMaxConns(25),
postgres.WithTimeout(5*time.Second),
postgres.WithLogger(logger),
)
```
**Hypothesis:** The Go team built a language around simplicity. Config structs are simple — they're just data, they compose trivially, they're discoverable via godoc. Functional options emerged from the community (Dave Cheney, Rob Pike) to solve API evolution in libraries with backward-compatibility constraints. The Go team never faced this pressure because the stdlib can break between major versions (Go 1 → Go 2). Open-source library authors can't — hence the community invented functional options as a compatibility tool.
**Who's right:** Config structs for 95% of cases. Functional options only matter when you ship a library where adding a field to a struct would be a breaking change (rare with Go's zero-value semantics).
---
### 3. Anti-pattern Depth ("When NOT to Use")
**Our pattern says:** Every pattern gets a detailed "When NOT to Use" section with over-application examples.
**Official guide says:** States the positive rule and moves on. No systematic exploration of misapplication.
**Example — what we cover that official guides don't:**
```go
// Our patterns flag this as interface over-application:
type Logger interface {
Info(msg string, args ...any)
Warn(msg string, args ...any)
Error(msg string, args ...any)
Debug(msg string, args ...any)
WithGroup(name string) Logger
With(args ...any) Logger
}
// Over-engineered: you have exactly one logger. This interface
// exists "for testing" — but you could just pass *slog.Logger.
```
**Official guide equivalent:** "Do not define interfaces before they are used" (one sentence, no example of what over-application looks like).
**Hypothesis:** The official guides assume a reader who exercises good judgment. They state principles and trust engineers to apply them correctly. Our patterns are for engineers who might *mechanically* apply rules ("always use interfaces!") without understanding boundaries. The guides were written by the Go team for experienced engineers at Google. Our patterns serve a broader audience that includes people who read "accept interfaces" and then create a 12-method interface for their only implementation.
**Who's right:** Both — different audiences. Official guides are more elegant. Our patterns are more defensive.
---
### 4. Named Return Values
**Our pattern says:** "Named returns for documentation and defer." Focuses on the positive case.
**Official guide says:** Default to unnamed. Only use names when same-typed returns need disambiguation.
**Example — what the official guide warns against:**
```go
// DON'T — named return adds noise without clarity:
func (n *Node) Parent1() (node *Node) { ... }
// The word "node" appears 3 times. The return name adds nothing.
// DON'T — named returns just to avoid declaring a var:
func (f *Foo) Bar() (val int, err error) {
val = computeSomething()
// ...only using it to skip `var val int`
}
```
**Example — when named returns genuinely help (both agree):**
```go
// DO — disambiguates same-typed returns:
func (f *Foo) Location() (lat, long float64) { ... }
// Without names, caller can't tell which float is which.
```
**Hypothesis:** Named returns became idiomatic early in Go's history (pre-1.0 code, the tour, early blog posts). Over time, the community realized they're usually noise — they clutter godoc, enable naked returns (which hurt readability), and suggest unused complexity. The official guide evolved to push back. Our patterns, extracted from stdlib written in that earlier era, still reflect the more liberal usage.
**Who's right:** Official guide. Default to unnamed, use names only when they genuinely disambiguate for the caller.
---
## Beyond Official (8 patterns guides don't cover)
| Pattern | Why guides omit it | Who needs it |
|---------|-------------------|--------------|
| Interface Upgrades (WriterTo in io.Copy) | Too advanced for style guide | Framework/infrastructure authors |
| Zero-Value Usability | Demonstrated but never articulated | Library designers |
| Compile-time checks (`var _ I = (*T)(nil)`) | Optional — compiler catches at usage | Exported type authors |
| Adapter Pattern (HandlerFunc) | Architectural, not stylistic | API designers |
| errors.Join | Too new (Go 1.20) — guides haven't caught up | Everyone (eventually) |
| Copy Protection (noCopy/copyCheck) | Implementation detail for library authors | sync, strings.Builder-style types |
| Background Worker + Context Shutdown | Principle stated, implementation not shown | Application developers |
| Layered API (Open/Create/OpenFile) | API design methodology, not code style | Public library authors |
---
## Meta-Assessment
**Why do they diverge at all?**
Three forces:
1. **Audience gap.** Official guides target "the next Googler who reads your code." Our patterns target "the engineer trying to write stdlib-quality code." Different starting assumptions about what the reader already knows.
2. **Time gap.** The stdlib was written over 15 years. Patterns accumulated organically. Official guides were written *after* the patterns existed — they're retrospective prescriptions. Some patterns (functional options, named returns) were endorsed early and later reconsidered.
3. **Abstraction level.** Style guides operate at the *line/function* level (naming, formatting, error handling). Our patterns operate at the *design* level (how types compose, when to add indirection, API surface evolution). These are different conversations that happen to touch the same code.
**The meta-rule:** When writing new code, follow official guides by default. When *designing* a system or reviewing architecture, consult our patterns for how mature code solves similar problems.