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

9.0 KiB

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:

// 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:

// 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):

// 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:

// 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:

// 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:

// 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):

// 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.