- Tests: use
make testas the default. Only usego test ...when you must pass specific flags (e.g.-run,-count,-race, build tags, etc.) or need to debug a specific test in isolation. - Lint: use
make lintas the default. Only callgolangci-lint ...directly when you must pass specific flags. - Always run test and lint prior to considering a task complete, unless told otherwise (or if you did not touch any Go code/tests).
- If your execution environment has a sandbox/permission model, run
make testandmake lintunsandboxed (full permissions) so results match local dev and CI.
- Run all tests with race detector:
make test/race - Run single test:
cd $MODULE && go test ./path/to/package -run TestName - Run benchmark:
make bench - Generate sqlc:
make generate. Use this any time a.sqlfile has been modified and we need to then regenerate.sql.gofiles from it. - Run tidy when deps change:
make tidy
- Imports: use gci sections - Standard, Default, github.com/riverqueue.
- Formatting: use gofmt, gofumpt, goimports.
- JSON tags: use snake_case for JSON field tags.
- Dependencies: minimize external dependencies beyond standard library and pgx.
- SQL access (non-test code): avoid ad-hoc SQL strings in library/runtime code. Add or extend a sqlc query, regenerate with
make generate, and expose it through the driver interface. Keep direct SQL for tests and benchmark/admin utilities only (for example,pg_stat_statementsandVACUUM). - Error handling: prefer context-rich errors; review linting rules before disabling them.
- Testing: use require variants instead of assert.
- Helpers: use
Funcsuffix for function variables, notFn. - Documentation: include comments for exported functions and types.
- Naming: use idiomatic Go names; see
.golangci.yamlfor allowed short variable names.
- Package names are lowercase, short, and representative; avoid
common,util, or overly broad names. - Use singular package names; avoid plurals like
httputils. - Keep import paths clean; avoid
src/,pkg/, or other repo-structure leakage. - Organize by responsibility instead of
models/typesbuckets; keep types close to usage. - Do not export identifiers from
mainpackages that only build binaries. - Add package docs, and use
doc.gowhen documentation is long.
Alphabetization is important when adding new code (do not reorganize existing code unless asked).
- Types should be sorted alphabetically by name.
- Struct field definitions on a type should be sorted alphabetically by name, unless there is a good reason to deviate (examples: ID fields first, grouping mutexed fields after a mutex, etc.).
- When declaring an instance of a struct, fields should be sorted alphabetically by name unless a similar deviation is justified.
- When defining methods on a type, they should be sorted alphabetically by name.
- Constructors should come immediately after the type definition.
- Keep all methods for a type grouped together, immediately after the type definition and any constructor(s), organized alphabetically by name. Do not intersperse methods with other types or functions, except in special cases where a small utility type is needed to support a method and not used elsewhere.
- In unit tests, the outer test blocks should be sorted alphabetically by name. Inner test blocks should also be sorted alphabetically by name within the outer block.
This repo uses a parallel test bundle pattern (inspired by Brandur's write-up: https://brandur.org/fragments/parallel-test-bundle) to keep parallel subtests isolated and setup/fixtures DRY.
- Always opt into parallel:
- Top-level tests: the first statement in every
TestXxxshould bet.Parallel(). - Subtests: the first statement in every
t.Run(..., func(t *testing.T) { ... })should bet.Parallel(), unless the subtest is intentionally non-parallel and includes a short comment explaining why.
- Top-level tests: the first statement in every
- Statement ordering and spacing:
- Top-level tests: use
t.Parallel(), then a blank line, then test preamble (ctx,setup, helpers), then a blank line before subtests/assertions. - Subtests: use
t.Parallel(), then a blank line, then subtest preamble. - If a subtest calls
setup(...), prefer one blank line after the setup assignment before assertions/actions. - For tiny/obvious subtests (one short statement after
t.Parallel()orsetup(...)), omitting one of these blank lines is acceptable.
- Top-level tests: use
- Context and setup ordering:
- If
setupneedsctx(setup(ctx, t)), assign/derivectxbefore callingsetup. - If
setupdoes not needctx(setup(t)), callsetupfirst and derive specialized contexts (WithCancel,WithTimeout) close to where they are used. - Avoid creating/deriving
ctxfar from usage unless shared setup requires it.
- If
- Prefer local bundles:
- Define a
type testBundle struct { ... }inside theTestXxxfunction containing the system under test and any fixtures frequently used across subtests. - Each parallel subtest should call
setup(t)to get a fresh bundle. Avoid sharing mutable state across parallel subtests.
- Define a
setuphelper rules:- Define
setupas a local closure in the test:setup := func(t *testing.T) *testBundle { ... }- Always call
t.Helper()at the top ofsetup.
- Only accept a context parameter if it is needed:
- Default:
setup(t)should not takectx. - If setup must derive/seed a context: prefer returning it:
setup := func(t *testing.T) (*testBundle, context.Context). - If setup must be passed an existing context: accept
ctxas the first parameter:setup := func(ctx context.Context, t *testing.T) *testBundle.
- Default:
- Keep
setupdeterministic and self-contained; it should only use the*testing.T(andctxif explicitly required) passed in.
- Define
- Test signal instrumentation:
- Prefer
rivershared/testsignal.TestSignalin a...TestSignalsstruct with anInit(tb)helper over ad hocchan struct{}fields in spies/fakes. - Keep test-signal structs zero-value by default and call
Init(t)only in tests that need to observe those signals. - Wait for async events with
WaitOrTimeout(). For negative assertions, useRequireEmpty()orWaitC()with a timeout select. - Avoid custom channel signaling helpers and hand-managed channel capacities unless there is a specific, documented reason.
- Prefer
Template:
func TestThing(t *testing.T) {
t.Parallel()
type testBundle struct {
// Put SUT + common fixtures here.
}
setup := func(t *testing.T) *testBundle {
t.Helper()
return &testBundle{}
}
t.Run("CaseName", func(t *testing.T) {
t.Parallel()
bundle := setup(t)
// ... use `bundle` in assertions/actions ...
})
t.Run("CaseNameWithCtxRequiredBySetup", func(t *testing.T) {
t.Parallel()
setupWithCtx := func(ctx context.Context, t *testing.T) *testBundle {
t.Helper()
_ = ctx
return &testBundle{}
}
ctx := context.Background()
bundle := setupWithCtx(ctx, t)
// ... use `bundle` in assertions/actions ...
})
}