This repo's rules for Go, where they differ from or sharpen general Go practice. The rules below apply to every change; the references hold the detail and worked examples.
Where to Look
Route to the right reference before reading further:
| If you're working on… | Load this |
|---|---|
| Goroutines, channels, sync primitives, errgroup, leaks, races | references/concurrency.md |
Adding a sentinel or error type, translating errors between packages or to HTTP responses, logging where an error came from, cleanup in defer, panic |
references/errors.md |
| HTTP middleware: request Content-Type, CSRF, session cookie attributes | references/middleware.md |
| Storing session or reset tokens, password reset or change, encrypting a column | references/secrets.md |
Giving main or a component a logger, choosing a level, CLI output vs logs |
references/logging.md |
| Interface design, zero values, identifier types, typed value sets, anti-patterns | references/patterns.md |
| Writing or reviewing tests, benchmarks, fuzzing, test databases | the go-testing skill |
Optional fields or return values — when to use Option[T] |
references/optional.md |
| Pointer vs value receivers, mutation vs optionality | references/value-vs-pointer.md |
Scaffolding a Go HTTP client for a third-party API, writing a NewXxxClient, or attaching auth to outbound HTTP calls |
references/http-client.md |
Shelling out with os/exec to git, gh or another CLI, parsing its --json output, faking it in tests |
references/exec.md |
| Storing data in SQL: store package boundary, transactions, NULLs, constraint errors, pool setup | references/database.md |
Rendering html/template for htmx: page vs fragment vs redirect, HX-* headers, .tmpl files |
the htmx-go-templates skill |
Errors and Logging
Nearly every change returns an error or reports one, so these rules apply to all Go work, not only to a task described as error handling:
- Wrap with
fmt.Errorf("<verb> <noun>: %w", err). Error strings are lowercase, with no punctuation and no "failed to". - Handle an error once: return it with context, or log it at the top of the chain. Never both.
- Export a sentinel only when a caller in another package branches on it. Define it where the condition is detected, and translate a dependency's errors into your own at the package seam.
- Never discard an error with
_ =. Join a cleanup error into the returned error, or log it atWarnwhen there is nothing to return to. paniconly for programmer errors:Musthelpers on constant input, impossible cases.- Only components where an error stops travelling (handlers, workers, long-lived services) take
an injected
*slog.Logger, as a constructor field. Code below them returns errors and takes no logger.mainbuilds the one logger; nothing callslog.*,slog.Infoorslog.Default(). - Log messages are constant; variable data goes in snake_case key-value attributes, with
errfor the error. Never log secrets or personal data. - Output the user asked for goes to an injected
io.Writer, not to the logger.
A routine return fmt.Errorf(...) or log line needs only these rules. Load references/errors.md
when adding a sentinel or error type, turning an error into an HTTP response, wrapping a driver or
outbound-call error, or handling a cleanup error, and references/logging.md when
wiring a logger, choosing a level or separating CLI output.
Testing
Writing, editing or reviewing a _test.go file: load the go-testing skill, which owns how Go
tests are written, including test databases. What to test (the kinds of test, what each is
worth, the reusable patterns) is the repo's testing-policy when it ships one.
Code Organisation
- Applications keep
mainincmd/<app>/main.go, which only wires dependencies, and everything else underinternal/.pkg/is for library modules imported from outside the repo; in an application it signals a public API that doesn't exist. - Extract shared logic on the third repetition across meaningfully different packages, not
the second. A helper coupled to one domain lives in
internal/next to its primary caller. - Name a package by what it does (
stringx,idgen,timeutil).utilsis allowed only when it is used across many packages and imports nothing frominternal/.helpersandcommonare never allowed: a real name is waiting to be found. - No package-level state such as a global
*sql.DBset ininit(). Pass dependencies through constructors. mainparses the environment once into a typed config struct, validates it, and exits on a bad value at startup rather than on the first request that needs it. Constructors receive only the fields they use, not the whole struct. Nothing belowcmd/callsos.Getenv.
Function Design
-
Past 3–4 arguments, group them into a struct, or use functional options (
opts ...Option) for a constructor that will grow; a new option is backward-compatible, a new positional argument is not. -
Declare complex arguments as named locals before the call instead of nesting constructor calls inline:
sentAt := time.Now().Add(-24 * time.Hour) priority := Priority(3) client.Send(NewMessage(sender, recipient, sentAt, priority)) -
Guard and return early; the happy path stays at the bottom, unindented, with no
elseafter areturn. Useswitchoverif/else ifchains. -
Use pointers for mutation and measurably large structs, values for small immutable types. Never use
*Tto mean "might not be set"; that isOption[T].
Naming and Comments
Name functions by what they return or do (ParseConfig, IsExpired) and booleans as predicates
(canRetry). Abbreviate only by Go convention (ctx, err, id, r, w).
~/.claude/rules/comments.md decides what earns a comment. It is the authority and this
skill does not restate or soften it. Read it before editing a file and apply it as written,
including its line cap and its exported-identifiers-only rule for doc comments. internal/
is not public API. When reviewing Go code for comment quality, use the clean-comments skill.
Do not instruct it to match the file's existing comment density.
Tooling
Run gofmt, goimports, go vet ./... and golangci-lint run before finishing. Leave
mechanical rules to the linter rather than restating them here. Never add //nolint to existing
violations: fix them, or leave the rule off until they are fixed.
See Also
| Skill | When to reach for it |
|---|---|
go-testing |
How tests are written: assertions, doubles, tables, test databases |
tdd |
TDD cycle, red-green-refactor, writing the test first |
clean-comments |
Audit and remove explanatory comments across Go code |
improve-codebase-architecture |
Identify structural improvements, shallow modules |
simplify |
Review changed code for quality, reuse, and efficiency after implementation |