5.6 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. Demonstrates: structured logging (slog), config (flag), consumer-defined interfaces + manual fakes, result type (happy-path error handling), linting (golangci-lint), security scanning (gosec, govulncheck), git hooks, devcontainer, VSCode tasks.
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.
Module: gitea.djmil.dev/go/template — update this when you fork.
Project structure
cmd/app/main.go composition root — wires deps, no logic here
internal/config/ flag-based config loader (config.Load)
internal/logger/ slog wrapper with WithField / WithFields
internal/greeter/ Example domain package (delete or repurpose)
pkg/result/ Example publishable package (Result/Expect types)
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
gitea.djmil.dev/go/template/... - Packages — keep
cmd/thin (wiring only); business 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 — wrap with
fmt.Errorf("context: %w", err); never swallow errors silently - Logging — use
log.WithField("key", val)for structured context; neverfmt.Sprintfin log messages;log/slogis the backend - Config — all configuration through
internal/config(flag-parsed); no hard-coded values in logic 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) - Prefer explicit over clever; PoC code should be readable first
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 - 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
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/
VSCode: Ctrl+Shift+B = build, Ctrl+Shift+T = test.
Debug: use launch config "Debug: app" (F5).
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.