Patterns and conventions for writing Go code. Covers API design, error handling, concurrency, interfaces, safety, performance, naming, testing, functional options, logging, and deterministic simulation testing.
Resources
1Install
npx skillscat add stuckinforloop/harness/go-write Install via the SkillsCat registry.
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.