Stack conventions
Go
Go 1.22+ services, command-line tools and libraries, using the standard library first and tested with the testing package.
Go 1.22+ services, command-line tools and libraries, using the standard library first and tested with the testing package.
Used for: go.
Principles
- Write code that gofmt leaves unchanged; never hand-format or argue about layout.
- Accept interfaces, return concrete types; define small interfaces in the package that consumes them.
- Treat errors as values; handle each error once, either by returning it with context or by logging it, never both.
- Wrap errors with fmt.Errorf("doing x: %w", err) and inspect them with errors.Is and errors.As, not string comparison.
- Pass context.Context as the first parameter of anything that does I/O or may block, and honour its cancellation.
- Every goroutine has an owner that knows how and when it stops; never start one you cannot stop.
- Make the zero value useful and prefer clarity over cleverness; a little copying is better than a little dependency.
- Keep packages cohesive and named for what they provide; avoid util, common, helpers and base packages.
- Use the standard library first (net/http, log/slog, encoding/json, testing, slices, maps) before adding a module.
- Write table-driven tests with t.Run subtests and useful failure messages (got X, want Y).
Conventions
Accessibility
Concurrency
- Do not communicate by sharing memory; share memory by communicating, but use a sync.Mutex when it is simpler.
- Prefer synchronous functions; let the caller add concurrency.
- Make goroutine lifetimes obvious; every goroutine exits on ctx.Done() or channel close; use sync.WaitGroup or errgroup to wait.
- Only the sender closes a channel; never close a channel from the receiver or close it twice.
- Do not copy values containing a sync.Mutex; pass pointers; keep the mutex next to the fields it guards.
- Do not store context.Context in structs; pass it through calls.
Dependencies
- Keep go.mod and go.sum committed and tidy (go mod tidy produces no diff).
- Prefer the standard library and well-maintained modules; review transitive dependencies before adding one.
- Use a tool directive in go.mod (Go 1.24+) for development tools instead of a tools.go file where the go version allows.
- Bump the go directive deliberately; it sets language semantics (for example per-iteration loop variables since 1.22).
Documentation
- Every exported identifier has a doc comment that starts with its name and is a complete sentence.
- Each package has a package comment (in doc.go for large packages).
- Use runnable Example functions in _test.go files for non-trivial APIs.
- Mark deprecations with a "Deprecated:" paragraph.
Errors
- Return error as the last result; check it immediately; keep the happy path unindented (early return).
- Error strings are lower case without trailing punctuation, because they are wrapped into other messages.
- Add context when returning (fmt.Errorf("load config %s: %w", path, err)); use %v instead of %w deliberately when you do not want callers to depend on the cause.
- Do not panic for expected failures; panic only for programmer errors, and never across a package API boundary.
- Use errors.Join when several independent errors must all be reported.
Logging
- Use log/slog with structured key-value attributes; inject the *slog.Logger rather than using the global default in libraries.
- Libraries return errors and generally do not log; the application decides what to log.
- Never log secrets, tokens or full request bodies; log identifiers instead.
- Use log.Fatal only in main; it skips deferred functions.
Naming
- Package names are short, lower case, single words, without underscores; do not repeat the package name in exported identifiers (http.Server, not http.HTTPServer).
- Use MixedCaps; initialisms keep a consistent case (ID, URL, HTTP: userID, ServeHTTP, not UserId).
- Getters have no Get prefix (Owner(), SetOwner()); one-method interfaces end in -er (Reader, Closer).
- Short receiver names (one or two letters) used consistently across methods; never self or this.
- Local variable names get longer the further they are from their use; i, n, err, ctx are fine in short scopes.
Performance
- Measure with benchmarks and pprof before optimising; avoid premature sync.Pool or unsafe.
- Preallocate slices and maps when the size is known (make([]T, 0, n)).
- Use strings.Builder or bytes.Buffer for building strings in loops.
- Stream large bodies with io.Reader / io.Copy instead of reading them fully into memory.
- Always close response bodies and rows (defer resp.Body.Close(), defer rows.Close()) and check rows.Err().
Security
- Use database/sql placeholders, never string-built SQL; use html/template (not text/template) for HTML output.
- Set timeouts on http.Server (ReadHeaderTimeout at least) and on http.Client; the zero-value client has no timeout.
- Use crypto/rand for secrets and tokens, never math/rand; compare secrets with crypto/subtle.ConstantTimeCompare.
- Validate and clean user-supplied file paths (os.Root in Go 1.24+ or filepath.IsLocal) to prevent traversal.
- Run govulncheck to find vulnerable dependencies that are actually reachable.
- Never pass external input to exec.Command through a shell (
sh -c); pass arguments separately (command injection).
Structure
- One module per repository unless there is a clear reason; go.mod at the root with an accurate go directive.
- Executables under cmd/
/main.go; code that must not be imported by other modules under internal/. - Avoid a generic pkg/ directory and deep nesting; organise by domain, not by layer (no models/, controllers/ packages).
- Keep main small; put logic in importable packages that tests can exercise.
- Avoid init() for anything other than trivial registration; no hidden I/O at import time.
Testing
- Table-driven tests with named cases and t.Run; call t.Parallel() where tests are independent.
- Failure messages state the function, inputs, got and want, in that order (got before want).
- Use t.Helper() in helpers, t.Cleanup for teardown, t.TempDir for files, t.Context (Go 1.24+) where available.
- Prefer the standard testing package and go-cmp (cmp.Diff) for comparisons; avoid assertion frameworks that hide failures.
- Use httptest.NewServer / httptest.NewRecorder for HTTP; run go test -race in CI; use fuzz tests (func FuzzX) for parsers.
- Use testing.B with b.Loop() (Go 1.24+) for benchmarks.
- Unit tests sit beside the code in package
x(orx_testfor the public API); integration tests needing a database or network use a build tag ortesting.Short()and real dependencies in containers. - Done means tested: new or changed behaviour ships with tests in the same change, and a fixed bug gets a regression test that fails without the fix.
Review checklist
- Is the code gofmt/goimports clean and does go vet ./... report nothing?
- Is every returned error checked, wrapped with useful context (%w) and handled exactly once?
- Are errors compared with errors.Is/errors.As rather than by string or ==, where wrapping is possible?
- Does every function that performs I/O or may block accept and honour a context.Context?
- Does every new goroutine have a clear owner and a guaranteed exit path?
- Are shared variables protected (mutex, channel or atomic) and would go test -race pass?
- Are interfaces small and declared by the consumer, with constructors returning concrete types?
- Are HTTP clients and servers configured with timeouts, and are response bodies and rows closed?
- Is SQL parameterised and HTML rendered through html/template?
- Are new tests table-driven, with failure messages that show got and want?
- Do exported identifiers and packages have doc comments starting with the name?
- Is go.mod tidy, and is any new dependency justified over the standard library?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
gofmt |
format | gofmt | no |
go-vet |
static | go vet | no |
golangci-lint |
lint | golangci-lint | no |
staticcheck |
static | staticcheck | no |
govulncheck |
security | govulncheck | no |
go-test-race |
test | go test | no |
gitleaks |
security | gitleaks | no |