Go — Writing
Core Principles
-
APIs are forever. Exported symbols, function signatures, and interface methods cannot change without breaking callers.
-
Consistency over cleverness. Follow established patterns even when a "smarter" approach exists.
-
Safety by default. Prefer defensive copies, zero-value safety, and compile-time checks.
-
Handle errors exactly once. Either log or return — never both.
Reference Index
Reference Topics
api-design Export permanence, signature immutability, parameter objects, result objects
errors Handle-once, sentinel/structured/opaque strategy, wrapping, naming
interfaces Small interfaces, compile-time checks, consumer-side, upcasting, driver pattern
concurrency Mutex placement, channel sizing, goroutine lifecycle, context cancellation
worker-pools Channel-based pools, context cancellation, WaitGroup shutdown
safety Defer cleanup, type assertions, no panic, copy at boundaries
performance strconv over fmt , pre-allocation, strings.Builder
naming Package naming, unexported global prefix, import aliases, receiver names
declarations Short declarations, nil slices, zero-value structs, field names
organization Import grouping, function ordering, early returns, line length
testing Table-driven tests, subtests, testify , go.uber.org/goleak
functional-options Option interface, apply method, WithX constructors
logging slog handlers, typed attributes, LogValuer, context integration, log levels
deterministic-simulation-testing TimeNow function type, seeded *rand.Rand , fs.FS , seed reproducibility
When to Apply
Apply when:
-
Writing new Go code
-
Refactoring existing Go code
-
Generating Go tests
Do NOT apply to non-Go files (Dockerfiles, YAML, Makefiles, etc.).
Quick Reference
Errors: Wrap with fmt.Errorf("op: %w", err) . Lowercase, no "failed to". Handle once. Sentinel (ErrX ) for identity checks, structured (XError ) for data, opaque %w for propagation.
Interfaces: Keep to 1-3 methods. Verify with var _ I = (*T)(nil) . Define at consumer. Driver pattern for extensibility.
Concurrency: Mutex as unexported field above guarded fields. Channels: size 0 or 1. Every goroutine needs stop signal + wait mechanism.
Safety: Two-value type assertions. Defer after acquire. No panic in libraries. Copy slices/maps at boundaries.
Performance: strconv
fmt.Sprint . Pre-allocate: make([]T, 0, n) . strings.Builder for concatenation.
Style: := for locals. Named struct fields. Imports: stdlib → external. Early returns. ~99 char lines.
Testing: Table-driven []struct with t.Run . require for preconditions, assert for checks.
Options: Option interface with apply(*options) . WithX constructors. Defaults in constructor.
Logging: Use log/slog with typed attributes (slog.String , slog.Int ). JSONHandler in production, TextHandler in dev. Accept *slog.Logger as dependency. Implement LogValuer for sensitive types. Static messages, variable data in attributes.
DST: Inject non-deterministic sources via function types (TimeNow ) and struct fields (*rand.Rand ). Use fs.FS for I/O. Log seeds. Sort map iterations. Assert invariants. Match abstraction to need.