feat: initial Go patterns guide from stdlib + Kubernetes source study
9 pattern files covering stdlib (structs, interfaces, API conventions, docs, style), Kubernetes (controller/reconciler, informer/cache, leader election, code generation), comparison (stdlib vs K8s approaches), and anti-patterns. All patterns cite exact source files and line numbers.
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
# Code Style Patterns in the Go Standard Library
|
||||
|
||||
## 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` line 130 (`URL`), `net/http/server.go` line 3041 (`TLSConfig`), `encoding/json/stream.go` line 280 (`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:3041
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// encoding/json/stream.go:280
|
||||
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` line 645, `os/file.go` lines 747–750, `encoding/json/stream.go` lines 280–281
|
||||
|
||||
**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.
|
||||
|
||||
**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:280-281
|
||||
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` lines 87, 100, 314, 387; `os/file.go` lines 140, 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` lines 3173–3174, `net/http/example_handle_test.go` lines 21–22
|
||||
|
||||
**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:3173-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` lines 14–27, `os/error.go` lines 46–67
|
||||
|
||||
**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` lines 70–85, `time/time.go` lines 936–943
|
||||
|
||||
**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.
|
||||
|
||||
**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:936-943
|
||||
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` line 16
|
||||
|
||||
**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` lines 915–943
|
||||
|
||||
**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.
|
||||
|
||||
**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` lines 8–36
|
||||
|
||||
**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:8-36
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
// ... more stdlib ...
|
||||
"time"
|
||||
_ "unsafe" // for linkname
|
||||
|
||||
"golang.org/x/net/http/httpguts"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user