8.3 KiB
CLAUDE.md — Agent Instructions
This file is read automatically by Claude Code at the start of every session. Keep it concise — the agent needs signal, not essays.
Project overview
Go 1.25 template for PoC, hobby projects, and small publishable packages. Stack: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck).
Key constraint: go.mod stays free of dev tool deps (tools are pinned in tools.versions and run via
go run tool@version) so packages published from this repo have a clean module graph for consumers.
Design philosophy
The Twelve-Factor App is the primary reference for default implementation choices. Follow its recommendations unless circumstances force a deviation — and document the reason when you do.
Key factors that shape this codebase most directly:
- Logs (factor XI) — the app writes log events to
stderras an unbuffered stream. It never opens log files or manages rotation. The execution environment (shell, systemd, Docker, k8s) routes and stores the stream. Human-readable program output goes tostdout(fmt.Print*). - Config (factor III) — all configuration comes from the environment (flags in this template; env vars are the 12-factor ideal). No hard-coded values in logic packages.
- Dependencies (factor II) — explicit declaration; dev tools are
pinned in
tools.versionsand kept out ofgo.modso published packages have a clean module graph.
Project structure
cmd/app/ CLI entrypoint: parses flags, wires dependencies, and expresses
what the program does in high-level readable steps — calls internal/
and pkg/ packages; no domain logic lives here
internal/ domain-specific packages; logical grouping of substantial code into
digestible types and interfaces; not importable outside this module
pkg/ generic, publishable packages reusable across projects; no
assumptions about the calling application
tools.versions Pinned tool versions (sourced by Makefile and pre-push hook)
.golangci.yml Linter rules
.githooks/pre-push Runs gofmt + go vet + golangci-lint + gosec before push
Project rules
- Module imports — always use the full module path from
go.mod(never relative imports) - Packages —
cmd/owns CLI parsing, dependency wiring, and high-level orchestration; domain logic belongs ininternal/ - Types — expose concrete types from constructors (
New(...) *Type); never wrap in an interface at the implementation site. Consumers define their own interfaces if they need one (Go's implicit satisfaction makes this free) - Errors —
pkg/resultis a convenience tool for removing error-threading clutter from application logic; use it as follows:pkg/libraries only returnresult.Expect[T]— never call.Expect(),.Must(), or.Expectf()inside library code; those methods exit the goroutine viaruntime.Goexitand are only safe in application-layer code protected by a boundary- application code (
cmd/, HTTP handlers, etc.) chains.Expect("context")freely — each call exits the goroutine on failure and is caught at the entry point - top-level entry points defer
result.Catch(&err)(or useresult.Run(...)) to convert any result exit into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked result.Catchis incompatible with-tags result_goexit: it relies onrecover()which cannot interceptruntime.Goexit; preferresult.Run/result.Gowhich work in both builds- bridge existing
(T, error)stdlib/third-party calls withresult.Of(...):result.Of(os.ReadFile("cfg.json")).Expect("read config") - use
result.StackTrace(err)to retrieve the capture-site stack from a caught error - still use
fmt.Errorf("context: %w", err)when wrapping errors before constructing aresult.Fail
- Logging — logs go to
stderr(structured, machine-readable, per 12-factor XI); human output goes tostdoutviafmt.Print*. Uselog.WithField("key", val)for structured context; neverfmt.Sprintfin log messages;log/slogis the backend - Config — all configuration parsed in
cmd/app/config.go(flags); no hard-coded values ininternal/orpkg/packages
Code style
- Follow
gofmt+goimportsformatting (enforced by linter and git hook) - Imports: stdlib → blank line → external → blank line → internal (goimports handles this)
- Error variables:
errfor local,ErrFoofor package-level sentinels - Constructors:
New(deps...) *Typepattern - Comment every exported symbol (golangci-lint will warn if missing)
- Max line length: 120 chars (configured in
.golangci.yml)
Testing rules
- Tests use only the standard
testingpackage — no third-party assertion libraries - Test files:
package foo_test(black-box) unless white-box access is needed - Fake dependencies with manual fakes (implement the interface inline in
_test.go) - Use
logger.NewNop()when the test doesn't care about log output - Use
logger.NewWriter(&buf, "debug")in acceptance / integration tests that need to assert on log content - Table-driven tests with
t.Run("description", ...)for multiple cases - The race detector is enabled in CI (
make test-race); don't introduce data races - Never use
time.Sleepin tests; use channels ort.Cleanup - Use
gitea.djmil.dev/go/template/pkg/testutilhelpers instead of manual checks —ResultOk,ResultOkNotNil,ResultErrforresult.Expect[T];NoError,Error,ErrorContains,Equalfor plain values
Development commands
make init # first-time setup: fetch deps, configure git hooks
make tools # install tool binaries to GOPATH/bin (versions from tools.versions)
make build # compile to ./bin/app
make run # go run with default flags
make test # run all tests
make test-race # tests + race detector
make lint # go vet + golangci-lint
make lint-fix # go fix + golangci-lint auto-fix
make security # gosec + govulncheck
make release # list releases, or tag+push after full checks (make release VERSION=v0.1.0)
make clean # remove bin/
Adding new features (checklist)
- Write the implementation in
internal/<domain>/— return a concrete*Type, no interface at the implementation site - In the consumer package (or
_test.go), declare a minimal interface covering only the methods you call - Write unit tests using a manual fake that satisfies that interface
- Wire the concrete type in
cmd/app/main.go - Run
make lint testbefore committing
Known pitfalls
govulncheckmakes network calls; excluded from pre-push hook (run manually)- Tool versions live in
tools.versions— edit that file to upgrade, both Makefile and hook pick it up go run tool@versionis used in lint/security targets; Go caches downloads so subsequent runs are fastmake toolsinstalls binaries toGOPATH/binfor IDE integration (e.g. dlv for the debugger)go fixrewrites source files; runmake lint-fixbefore committing after a Go version bump
Recent work
- 2026-03-29 — Stripped to stdlib-only: removed testify/mockery→manual fakes, zap→slog, viper→flag.
- 2026-03-29 — Pre-commit hook moved to pre-push; go vet + go fix added to lint pipeline.
- 2026-03-29 — Fixed stale docs and .golangci.yml local-prefixes; .vscode launch configs use CLI flags.
- 2026-04-01 — Replaced tools.go/go.mod pinning with tools.versions + go run tool@version; go.mod is now free of dev tool deps.
- 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes.
- 2026-04-23 — Documented result layering rule: pkg/ libraries only return Expect[T]; .Expect()/.Must() calls belong in application-layer code.