docs: enhance Go divergence analysis with code examples + hypotheses

This commit is contained in:
Rodin
2026-04-30 07:57:37 -07:00
parent ca07514d9c
commit 6c1fa35b1c
+129 -123
View File
@@ -9,171 +9,177 @@
## Agreements (brief) ## Agreements (brief)
Our extracted patterns and the official Go guidelines **agree** on the following core principles: | Area | Official Source | Status |
|------|---------------|--------|
| Area | Official Source | Agreement | | Small interfaces (1-2 methods) | CodeReviewComments | Aligned |
|------|---------------|-----------| | Accept interfaces, return structs | CodeReviewComments | Aligned |
| Small interfaces (1-2 methods) | CodeReviewComments "Interfaces" | Both say interfaces belong in the consumer package, keep them small | | Don't define interfaces "for mocking" | CodeReviewComments | Aligned |
| Accept interfaces, return structs | CodeReviewComments "Interfaces" | Both recommend returning concrete types, accepting interfaces at boundaries | | Error strings: lowercase, no punctuation | CodeReviewComments | Aligned |
| Don't define interfaces "for mocking" | CodeReviewComments "Interfaces" | Both explicitly warn against this | | Handle errors, don't discard with `_` | CodeReviewComments | Aligned |
| Error strings: lowercase, no punctuation | CodeReviewComments "Error Strings" | Perfect alignment | | Indent error flow (happy path left) | CodeReviewComments | Aligned |
| Handle errors, don't discard with `_` | CodeReviewComments "Handle Errors" | Identical advice | | Receiver names: short, consistent | Google Style Decisions | Aligned |
| Indent error flow (happy path left-aligned) | CodeReviewComments "Indent Error Flow" | Our error-handling patterns show this | | MixedCaps / no underscores | Effective Go | Aligned |
| Receiver names: short, consistent, never this/self | CodeReviewComments "Receiver Names" + Google Style Decisions | Identical | | Acronyms all-caps (URL, ID, HTTP) | CodeReviewComments | Aligned |
| MixedCaps / no underscores | Effective Go + Google Style Guide | Perfect alignment | | gofmt is non-negotiable | All guides | Aligned |
| Acronyms all-caps (URL, ID, HTTP) | CodeReviewComments "Initialisms" + Google Style Decisions | Identical, including edge cases (gRPC, DDoS) | | Context as first param, never in structs | CodeReviewComments | Aligned |
| gofmt is non-negotiable | Effective Go + CodeReviewComments + Google Style | All agree: format with gofmt, no exceptions | | Don't Panic for normal errors | CodeReviewComments | Aligned |
| Context as first parameter, never in structs | CodeReviewComments "Contexts" | Our concurrency patterns match exactly |
| Don't Panic for normal errors | CodeReviewComments "Don't Panic" | Our Must pattern correctly limits panic to init-time programmer errors |
--- ---
## Divergences ## Divergences
### 1. Interface Definition: "Don't define before they are used" ### 1. Interface Definition Timing
**Our pattern:** We teach interface design extensively upfront — 10 patterns covering composition, adapters, optional interfaces, runtime upgrades, driver patterns. The framing assumes you're *designing* interfaces as a primary activity. **Our pattern says:** Design interfaces upfront — 10 patterns covering composition, adapters, optional interfaces.
**Official guide (CodeReviewComments):** "Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain." **Official guide says:** "Do not define interfaces before they are used."
**Why they differ:** Our patterns were extracted from the *standard library*, which is mature code where interfaces emerged from years of real usage. The official guide targets the *development process* — advising engineers not to pre-design interfaces before concrete use cases exist. The stdlib already had those use cases. Our patterns describe the *outcome* of good design; the official guide describes the *process* to arrive there. **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
}
```
**Assessment:** The official guide is more trustworthy for *new code*. Our patterns are better as reference material for *how interfaces should look* once the design has crystallized. **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 ### 2. Functional Options vs Config Structs
**Our pattern (package-design.md):** Documents both approaches but notes that the stdlib uses struct-based configuration (http.Server, tls.Config) and the functional options pattern "emerged from the community." Our table says config structs for "few options, all data (stdlib preference)" and functional options for "many options, some involve behavior, public API stability." **Our pattern says:** Both valid — functional options for "many options, some involve behavior, public API stability."
**Official guide (Google Style):** The Google Style Best Practices document does not mention functional options at all. The stdlib universally uses config structs with nil-means-default semantics. The Google style implicitly endorses config structs through its "least mechanism" principle: use the simplest tool that works. **Official guide says:** Nothing about functional options. Stdlib universally uses config structs. Google Style's "least mechanism" implies: use the simpler thing.
**Why they differ:** Our patterns try to be balanced by acknowledging community practice (Dave Cheney, Rob Pike blog posts). The official guides — both Go team and Google — appear to view config structs as sufficient and functional options as unnecessary complexity for most cases. **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
}
```
**Assessment:** The official position (config structs) is simpler and covers 95% of cases. Functional options add value primarily for libraries with evolving APIs where binary compatibility matters (rare outside large organizations). Our patterns are correct but may over-emphasize functional options by giving them equal billing. **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. "When NOT to Use" Sections (Anti-pattern Depth) ### 3. Anti-pattern Depth ("When NOT to Use")
**Our patterns:** Every pattern includes a detailed "When NOT to Use" section with over-application examples, showing what happens when the pattern is misapplied. This is ~40% of our content. **Our pattern says:** Every pattern gets a detailed "When NOT to Use" section with over-application examples.
**Official guides:** Rarely discuss over-application. Effective Go and CodeReviewComments state the positive rule ("do X") without exploring what happens when you cargo-cult it. The Google Style Guide's "Best Practices" section occasionally shows bad/good contrasts but doesn't systematically cover misapplication. **Official guide says:** States the positive rule and moves on. No systematic exploration of misapplication.
**Why they differ:** Our patterns were designed as *teaching material* for engineers who might mechanically apply patterns without understanding their boundaries. The official guides assume the reader exercises judgment and don't attempt to enumerate all failure modes. The Google style achieves this through the "Least mechanism" principle — a general guard against over-engineering. **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.
```
**Assessment:** Our "When NOT to Use" sections are genuinely valuable and fill a gap in official documentation. They're not contradicting official guidance; they're supplementing it with wisdom the guides assume you'll develop through experience. **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 ### 4. Named Return Values
**Our pattern (style.md):** "Named returns for documentation and defer." Suggests using named returns when they add documentary value or enable `defer` modification. **Our pattern says:** "Named returns for documentation and defer." Focuses on the positive case.
**Official guide (CodeReviewComments "Named Result Parameters"):** More cautious: "Don't name result parameters just to avoid declaring a var inside the function; that trades off a minor implementation brevity at the cost of unnecessary API verbosity." Also warns that named returns create repetitive godoc (e.g., `func (n *Node) Parent1() (node *Node)` stutters). **Official guide says:** Default to unnamed. Only use names when same-typed returns need disambiguation.
**Why they differ:** Our pattern focuses on the positive case (when named returns help). The official guide focuses more on the negative case (when named returns hurt readability). The CodeReviewComments stance is: avoid named returns by default, use them only when the function returns same-typed values and the names genuinely disambiguate for the caller. **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.
**Assessment:** The official guide's caution is correct. Named returns are over-used in practice. Our pattern should emphasize the "default to unnamed" stance more strongly. // 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 (patterns we extracted that guides don't cover) ## Beyond Official (8 patterns guides don't cover)
### 1. Interface Upgrade Pattern (WriterTo/ReaderFrom in io.Copy) | Pattern | Why guides omit it | Who needs it |
|---------|-------------------|--------------|
**Our pattern:** Documented as Pattern #9 in interfaces.md — functions check for richer interfaces at runtime to unlock optimizations (zero-copy sendfile, etc.) | Interface Upgrades (WriterTo in io.Copy) | Too advanced for style guide | Framework/infrastructure authors |
| Zero-Value Usability | Demonstrated but never articulated | Library designers |
**Official guides:** Not mentioned in Effective Go, CodeReviewComments, or Google Style. This is a stdlib-specific advanced technique. | Compile-time checks (`var _ I = (*T)(nil)`) | Optional — compiler catches at usage | Exported type authors |
| Adapter Pattern (HandlerFunc) | Architectural, not stylistic | API designers |
**Why it's beyond official:** This pattern requires deep understanding of type assertions, performance implications, and interface layering. The guides focus on code *most engineers write*; the upgrade pattern is for framework/infrastructure authors. | 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 |
--- ---
### 2. Zero-Value Usability (structs.md) ## Meta-Assessment
**Our pattern:** Structs should be designed so `var x T` immediately works. Nil fields fall back to sensible defaults at call time. **Why do they diverge at all?**
**Official guides:** Effective Go briefly mentions "the zero value of a sync.Mutex is an unlocked mutex" but doesn't elevate this to a design principle. CodeReviewComments doesn't address it. Google Style mentions it in passing ("zero value ready to use" in some library docs). Three forces:
**Why it's beyond official:** This is a *design philosophy* extracted from repeated stdlib patterns (http.Client, bytes.Buffer, strings.Builder). Official docs demonstrate it but never articulate it as a rule. 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. Compile-Time Interface Satisfaction (`var _ I = (*T)(nil)`) 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.
**Our pattern:** Dedicated section with examples from io, os, encoding/json, net/http. **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.
**Official guides:** Not mentioned in Effective Go, CodeReviewComments, or Google Style Guide. It's a community idiom that the stdlib uses but no official document recommends.
**Why it's beyond official:** This is a defensive programming technique. The guides likely omit it because it's considered optional — the compiler already catches interface mismatches at usage sites. Our pattern correctly notes it's most valuable for exported types implementing external interfaces.
---
### 4. The Adapter Pattern (HandlerFunc)
**Our pattern:** Full treatment of how `type Func func(...)` with a method bridges functions to interfaces.
**Official guides:** Effective Go mentions it very briefly. Neither CodeReviewComments nor Google Style discuss it systematically.
**Why it's beyond official:** It's a specific implementation technique, not a style concern. The guides focus on *how to write* code (style), not *architectural patterns* (design).
---
### 5. errors.Join — Multi-Error Aggregation
**Our pattern:** Full treatment of Go 1.20's `errors.Join` with when-to-use and when-not-to-use.
**Official guides:** Not in any official style guide (too new — CodeReviewComments predates it). The Go blog covered it but style guides haven't incorporated it.
**Why it's beyond official:** Official guides lag behind language evolution. Our patterns are current (Go 1.24/1.25 features like `errors.AsType` and `WaitGroup.Go`).
---
### 6. Copy Protection (noCopy, copyCheck)
**Our pattern:** Documents both `noCopy` (vet-detected) and `copyCheck` (runtime panics, strings.Builder).
**Official guides:** CodeReviewComments mentions "Copying" briefly (don't copy a Buffer), but doesn't explain the implementation patterns for enforcing it.
**Why it's beyond official:** Implementation-level concern for library authors, not application developers.
---
### 7. Background Worker with Context Shutdown
**Our pattern:** Full concurrency pattern showing `OpenDB`-style background goroutine + context cancellation.
**Official guides:** CodeReviewComments "Goroutine Lifetimes" says "make it clear when - or whether - they exit" and "document when and why the goroutines exit." But doesn't show the full structural pattern.
**Why it's beyond official:** The guides state the principle; we show the implementation. Application engineers need both.
---
### 8. Layered API (Open/Create/OpenFile)
**Our pattern:** Full treatment of convenience wrappers over a configurable core function.
**Official guides:** Not discussed in any official Go style document.
**Why it's beyond official:** This is API design methodology, not code style. The Go team demonstrates it consistently but never writes it down as a rule.
---
## Assessment: Which is More Trustworthy When They Conflict?
**The official guides are more trustworthy for:**
- Day-to-day coding decisions (naming, formatting, error handling)
- Determining whether to create an interface at all
- Named returns (be more conservative than our patterns suggest)
- Config approach (prefer structs over functional options)
**Our patterns are more trustworthy for:**
- Understanding *why* stdlib code looks the way it does
- Knowing when a pattern is being misapplied (our "When NOT to Use" sections)
- Advanced patterns for library/framework authors (interface upgrades, adapters, driver patterns)
- Current Go features (errors.Join, WaitGroup.Go, errors.AsType)
**The fundamental difference:** Official guides tell you *what to do*. Our patterns tell you *what the result looks like and why*. The guides are prescriptive process; our patterns are descriptive reference. When they disagree, the guides are usually right about the *default behavior* (e.g., "don't create interfaces prematurely"), while our patterns are right about the *goal state* (e.g., "mature interfaces look like this").
**For Aaron's work codebase:** Follow official guides as the default. Reference our patterns when designing libraries, APIs, or when you've already determined an interface/pattern is needed and want to implement it idiomatically.