docs: idiomatic Go patterns from stdlib + Kubernetes with source citations

This commit is contained in:
Rodin
2026-04-30 04:07:41 -07:00
commit b5d0544fd6
7 changed files with 3997 additions and 0 deletions
+551
View File
@@ -0,0 +1,551 @@
# Go Package Design Patterns
Patterns extracted from the Go standard library source code.
---
## 1. Package-Level Documentation
### Source: `src/io/io.go:5-13`, `src/sync/mutex.go:5-11`, `src/context/context.go:5-57`
```go
// src/io/io.go:5-13
// Package io provides basic interfaces to I/O primitives.
// Its primary job is to wrap existing implementations of such primitives,
// such as those in package os, into shared public interfaces that
// abstract the functionality, plus some other related primitives.
//
// Because these interfaces and primitives wrap lower-level operations with
// various implementations, unless otherwise informed clients should not
// assume they are safe for parallel execution.
package io
```
```go
// src/sync/mutex.go:5-11
// Package sync provides basic synchronization primitives such as mutual
// exclusion locks. Other than the Once and WaitGroup types, most are intended
// for use by low-level library routines. Higher-level synchronization is
// better done via channels and communication.
//
// Values containing the types defined in this package should not be copied.
package sync
```
### Why
The package comment:
1. **States the purpose** in one sentence
2. **Establishes contracts** (not safe for parallel execution, values must not be copied)
3. **Guides users** toward correct usage (prefer channels over mutexes)
4. **Appears before `package` keyword** — becomes `go doc` output
### Convention
- 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
### Anti-pattern
```go
// DON'T: No package comment
package myutil
// DON'T: Restate the obvious
// Package http provides HTTP stuff.
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
```
---
## 2. Package Naming
### Source: All stdlib packages follow these conventions
**Stdlib examples:**
- `io` — not `ioutil`, not `ioutils`
- `fmt` — not `format`, not `formatting`
- `sync` — not `synchronization`
- `net/http` — not `net/httpserver`
- `encoding/json` — not `encoding/jsonparser`
- `context` — not `ctx` or `contexts`
- `errors` — not `errs` or `errorhandling`
### Why
Go package names are:
- **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
// Good: package name provides context
http.Server // not http.HTTPServer
json.Encoder // not json.JSONEncoder
context.Context // the type IS the context
```
### Anti-pattern
```go
// DON'T: Stutter (repeat package name in exported identifiers)
package http
type HTTPServer struct{} // http.HTTPServer — redundant
func NewHTTPClient() // http.NewHTTPClient — say "http" twice
// DON'T: Use utility/helper package names
package utils // what does it DO?
package helpers // grab bag, no cohesion
package common // everything ends up here
// DON'T: Use plural when singular works
package requests // should be: package request
```
---
## 3. internal/ Packages — Restricting Visibility
### Source: `src/net/http/internal/`, `src/encoding/json/internal.go`
```
src/net/http/internal/
├── ascii/
├── chunked.go
├── common.go
├── http2/
├── httpcommon/
├── httpsfv/
├── sniff.go
└── testcert/
```
### Why
Packages under `internal/` can only be imported by code rooted at the parent of `internal`. For example:
- `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.
### Usage Guidelines
```
myproject/
├── internal/ # shared across the project, but not importable externally
│ ├── auth/
│ └── metrics/
├── cmd/
│ └── server/
└── pkg/ # actually public API (if you use this convention)
└── client/
```
### Anti-pattern
```go
// DON'T: Export implementation details that should be internal
package mylib
func HelperThatOnlyIUse() {} // pollutes API surface
// DON'T: Put everything in internal/ (nothing is reusable)
// Balance: internal/ for implementation; exported packages for contracts
```
---
## 4. Export Rules — The Capital Letter Boundary
### Source: Throughout stdlib — the convention is the language itself
```go
// src/io/io.go
var EOF = errors.New("EOF") // exported: uppercase
var errInvalidWrite = errors.New(...) // unexported: lowercase
// src/io/io.go:622-625
type teeReader struct { // unexported type
r Reader
w Writer
}
// src/io/io.go:618
func TeeReader(r Reader, w Writer) Reader { // exported constructor
return &teeReader{r, w}
}
```
### Why
The exported/unexported boundary is Go's encapsulation mechanism. `teeReader` is unexported because:
1. Users don't need to know its implementation
2. The return type is `Reader` (the interface) — maximum flexibility
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
```go
// DON'T: Export everything "just in case"
type Parser struct {
Input string // should this be settable? probably not
buffer []byte // internal state — definitely not
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
### Source: `src/net/http/http2.go:37`, `src/net/http/servemux121.go:31`
```go
// src/net/http/http2.go:37
func init() {
// register HTTP/2 protocol implementation
}
```
### Why
`init()` runs automatically at program start, in dependency order. The stdlib uses it for:
- **Driver registration** (database drivers register via init)
- **Protocol negotiation** (HTTP/2 registers its handler)
- **Configuration from build tags** (`servemux121.go` — compatibility shim)
### Rules
1. `init()` should have no side effects beyond registration
2. No errors should be possible (can't return error from init)
3. Keep them short — they block program startup
4. Prefer explicit initialization in `main()` when possible
### Anti-pattern
```go
// DON'T: Do heavy work in init
func init() {
db = connectToDatabase() // fails silently, crashes later
cache = loadGigabyteFile() // blocks startup
}
// DON'T: Use init for configuration
func init() {
port = os.Getenv("PORT") // harder to test, implicit dependency
}
// DO: Prefer explicit setup
func main() {
db, err := connectToDatabase()
if err != nil {
log.Fatal(err) // clear failure point
}
}
```
---
## 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 (Server, Transport, DB config via setters). The functional options pattern emerged from the community to solve the "many optional parameters" problem:
```go
// The pattern (not in stdlib, but idiom from Rob Pike/Dave Cheney):
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr, timeout: 30 * time.Second}
for _, opt := range opts {
opt(s)
}
return s
}
```
### What the stdlib uses instead: Config structs
```go
// src/net/http/server.go (Server struct acts as config)
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Handler: mux,
}
```
### When to use which
| Approach | When |
|----------|------|
| Config struct | Few options, all are data (stdlib preference) |
| Functional options | Many options, some involve behavior, public API stability matters |
| 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
### Source: `src/net/http/server.go:2639`, `src/database/sql/sql.go:836`
```go
// src/net/http/server.go:2639
func NewServeMux() *ServeMux {
return new(ServeMux)
}
// src/database/sql/sql.go:836-843
func OpenDB(c driver.Connector) *DB {
ctx, cancel := context.WithCancel(context.Background())
db := &DB{
connector: c,
openerCh: make(chan struct{}, connectionRequestQueueSize),
lastPut: make(map[*driverConn]string),
stop: cancel,
}
go db.connectionOpener(ctx)
return db
}
```
### Why
- `NewX()` when construction is trivial (just allocate)
- `OpenX()` or `NewXWithConfig()` when construction involves resources, validation, or can fail
- Return `*T` (pointer to concrete type), not an interface
The zero value should be usable where possible (`sync.Mutex`, `bytes.Buffer`), making constructors unnecessary.
### Anti-pattern
```go
// DON'T: Constructor that returns interface (hides useful methods)
func NewWriter() io.Writer { return &myWriter{} }
// DON'T: Require constructor when zero value works
type Buffer struct {
buf []byte
// ...
}
// var b bytes.Buffer ← just works, no New needed
```
---
## 8. Package Organization — One Concern Per Package
### Source: Standard library structure
```
src/
├── io/ # I/O interfaces + helpers
├── os/ # OS operations
├── net/ # network primitives
│ ├── http/ # HTTP protocol
│ └── url/ # URL parsing
├── encoding/ # encoding interfaces
│ ├── json/ # JSON codec
│ └── xml/ # XML codec
├── database/
│ └── sql/ # SQL database abstraction
│ └── driver/ # SPI for database drivers
└── context/ # cancellation propagation
```
### Why
Each package has a single, clear responsibility:
- `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
```go
// DON'T: Package per type
package user // just has User struct
package order // just has Order struct
package payment // just has Payment struct
// 50 packages with 1 file each — Go prefers fewer, larger packages
// DON'T: Circular dependencies
package a imports package b
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
### Source: `src/database/sql/sql.go` vs `src/database/sql/driver/driver.go`
Two distinct APIs in one subsystem:
**User-facing (database/sql):**
```go
db, _ := sql.Open("postgres", connStr)
rows, _ := db.QueryContext(ctx, "SELECT ...")
defer rows.Close()
for rows.Next() {
rows.Scan(&id, &name)
}
```
**Driver-facing (database/sql/driver):**
```go
type Driver interface {
Open(name string) (Conn, error)
}
type Conn interface {
Prepare(query string) (Stmt, error)
Close() error
Begin() (Tx, error)
}
```
### Why
The user never sees `driver.Conn`. The driver never sees `sql.DB`'s pool logic. Clean separation:
- 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
### Source: `src/context/context.go:132-164`, `src/net/http/server.go:244-252`
```go
// src/context/context.go:132-164 (from doc comment)
// Package user defines a User type that's stored in Contexts.
// package user
//
// import "context"
//
// type key int
//
// var userKey key
//
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// return u, ok
// }
```
```go
// src/net/http/server.go:244-252
var (
ServerContextKey = &contextKey{"http-server"}
LocalAddrContextKey = &contextKey{"local-addr"}
)
type contextKey struct {
name string
}
```
### Why
- **Unexported key type** prevents other packages from accessing or overwriting your values
- **Type-safe accessors** (`FromContext`) avoid type assertions at every call site
- **Pointer-based keys** (`&contextKey{...}`) guarantee uniqueness even with same string names
### Anti-pattern
```go
// DON'T: Use string keys (any package can collide)
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
ctx = context.WithValue(ctx, "timeout", 5*time.Second) // use function params!
```
---
## Summary: Package Design Principles
| Principle | Rule |
|-----------|------|
| Package comment | `"Package X does Y."` before `package` keyword |
| Naming | Short, lowercase, no stutter (`http.Server` not `http.HTTPServer`) |
| Encapsulation | `internal/` for shared-but-private code |
| Exports | Minimum viable surface; unexported by default |
| init() | Only for registration; keep trivial |
| Constructors | `NewX()``*T`; prefer usable zero values |
| Organization | One concern per package; no circular deps |
| API layers | Separate user-facing from implementor-facing (SPI) |
| Context values | Unexported key type + typed accessors |
| Configuration | Struct literals (stdlib) or functional options (community) |