template/CLAUDE.md

6.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. 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 in internal/
  • 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)
  • Errorspkg/result is the default error-handling mechanism for all code in this repo, including public APIs:
    • functions return result.Expect[T] instead of (T, error)
    • callers unwrap with .Expect("context") (panics with annotated error + stack trace) or .Must() (panics with raw error)
    • top-level entry points (e.g. cmd/ functions, HTTP handlers) defer result.Catch(&err) to convert any result panic into a normal Go error; genuine runtime panics (nil-deref, etc.) are re-panicked
    • bridge existing (T, error) stdlib/third-party calls with result.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 a result.Fail
  • Logging — use log.WithField("key", val) for structured context; never fmt.Sprintf in log messages; log/slog is the backend
  • Config — all configuration through internal/config (flag-parsed); no hard-coded values in logic packages

Code style

  • Follow gofmt + goimports formatting (enforced by linter and git hook)
  • Imports: stdlib → blank line → external → blank line → internal (goimports handles this)
  • Error variables: err for local, ErrFoo for package-level sentinels
  • Constructors: New(deps...) *Type pattern
  • 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 testing package — 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.Sleep in tests; use channels or t.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)

  1. Write the implementation in internal/<domain>/ — return a concrete *Type, no interface at the implementation site
  2. In the consumer package (or _test.go), declare a minimal interface covering only the methods you call
  3. Write unit tests using a manual fake that satisfies that interface
  4. Wire the concrete type in cmd/app/main.go
  5. Run make lint test before committing

Known pitfalls

  • govulncheck makes 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@version is used in lint/security targets; Go caches downloads so subsequent runs are fast
  • make tools installs binaries to GOPATH/bin for IDE integration (e.g. dlv for the debugger)
  • go fix rewrites source files; run make lint-fix before 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.