Go Rig
Strict design and testing discipline for Go projects.
This skill complements CLAUDE.md.
CLAUDE.md owns:
- Go version, toolchain, and commands
- Key style, error, context, and concurrency rules
.claude/rules/ owns:
- Go 1.26 idioms and go fix modernizer catalog (
go-idioms.md)
- Detailed style, API, documentation, and testing patterns (
go-patterns.md)
This skill adds:
- ATDD/TDD workflow
- design principles and abstraction discipline
- dependency injection discipline
- package-boundary judgment
- documentation discipline
- comment quality standards
- structured review process
Do not restate or override version-specific guidance from CLAUDE.md. If CLAUDE.md is stricter on a shared point, follow CLAUDE.md.
When to Use
Use this skill when:
- implementing a new feature or behavior increment
- refactoring Go code for clearer ownership or testability
- reviewing package boundaries or dependency flow
- replacing hidden collaborator construction with explicit injection
- tightening tests around user-visible or integration behavior
ATDD/TDD Workflow
Test-first is a design tool, not an afterthought.
- Acceptance first — define the boundary behavior before writing code
- Acceptance test — add or update an acceptance-level test if the project has that layer; otherwise express boundary behavior in the closest consumer-level test
- Smallest failing unit test — for the next behavior increment
- Minimal implementation — only enough to pass
- Refactor — improve readability and cohesion while green
- Repeat — next behavior increment
If repository policy does not allow automatic test execution, still design test-first and ask before running.
Test Coverage Expectations
Every meaningful change should cover:
- expected behavior (happy path)
- invalid input and validation failures
- edge cases and boundary values
- error and failure paths
- concurrency behavior when relevant
Use the project’s existing test layers where possible. Reach for acceptance tests when the change is user-visible or integration-heavy, and unit tests when isolating business rules or edge cases.
Definition Of Done
A change is not done when the code "works on one path." It is done when:
- acceptance behavior is specified at the right boundary
- the smallest relevant unit behavior is covered
- failure and edge behavior are covered
- code was refactored back to clarity after going green
- repo test/lint/static-analysis expectations were satisfied or explicitly deferred
Design Principles
Apply without ceremony — these guide decisions, not generate boilerplate.
SRP — each package, type, and function has one clear reason to change. Split when a change in one concern forces changes in an unrelated concern.
DRY — extract repeated validation, mapping, branching, and business rules. Do not DRY away incidental similarity — two things that look alike but change for different reasons should stay separate.
OCP — extend stable areas carefully, but do not invent indirection to satisfy the idea of extensibility. In Go, a concrete type with a small seam at the consumer is usually better than an abstract framework.
When applying SRP/DRY/OCP in Go, prefer deleting duplication caused by mixed responsibilities before introducing new abstractions. The first move is usually better boundaries, not more interfaces.
Abstraction Discipline
- Start with concrete types and direct calls
- Introduce an interface only when a real consumer needs substitution
- Prefer one seam at a boundary over many tiny abstractions in the core
- If an abstraction adds files, wiring, and names but no clear testability or ownership win, do not add it
Avoid:
- interface-per-struct
- repositories or services that only forward calls
- configuration objects passed everywhere to avoid choosing explicit parameters
- “future-proofing” abstractions without a concrete second implementation or consumer
Function Design
- A function should usually do one thing: validate, transform, orchestrate, persist, or render
- If a function mixes business rules with transport, storage, or logging details, split it
- Prefer early returns over nested condition pyramids
- Keep parameter lists explicit and intention-revealing; if many values travel together for one reason, introduce a small typed struct
- Use whitespace to separate logical phases so the control flow reads top to bottom
Refactor when a function:
- needs comments to explain the control flow
- mixes unrelated reasons to change
- carries mutable state across many screens of code
- repeats branching or validation logic that belongs in a helper or type method
Dependency Injection
- Constructors for types that must enforce invariants or own long-lived collaborators
- Function parameters for short-lived collaborators and pure logic
- Never construct DB clients, HTTP clients, loggers, or repositories inside domain methods
- No DI frameworks — explicit wiring only
- No hidden globals or singletons
- Prefer passing dependencies from the composition root (
main, wiring package, or test setup) instead of looking them up deep inside the call stack
- Inject seams for time, randomness, process execution, filesystem, and external I/O when behavior depends on them
- Do not hide dependencies behind package-level variables except in rare compatibility shims
// constructor injection for long-lived deps
func NewOrderService(store OrderStore, clock Clock) *OrderService {
return &OrderService{store: store, clock: clock}
}
// function parameter for short-lived/pure logic
func ValidateOrder(order Order, now time.Time) error {
if order.ExpiresAt.Before(now) {
return fmt.Errorf("order %s expired: %w", order.ID, ErrExpired)
}
return nil
}
Package Design
Organize by domain, not by technical layer.
- Group related domain logic together until splitting clearly improves cohesion
- Keep transport and storage near the owning domain in the repo when the service is small, but do not let core business logic depend on transport details
- Split files when doing so improves readability; file count is not a goal by itself
- Split packages only when coupling pressure is real, not speculative
Avoid:
- interface-per-struct without a consumer need
- deep layering in small services
internal/platform/ catch-all layers — keep cross-cutting concerns in focused packages (internal/config/, internal/db/)
- packages that combine unrelated domains because they share a datastore or transport
- "shared" packages that centralize unrelated helpers and create import gravity
Hardcoding And Configuration
- Do not hardcode URLs, ports, credentials, file paths, timeouts, feature flags, environment names, or dependency selection in core logic
- Domain invariants may be constants, but operational values should come from config, constructor parameters, or function arguments
- Prefer typed config structs validated at startup over scattered
os.Getenv calls
- Keep configuration loading at the edge; pass validated values inward
Type Discipline
- Model domain concepts with named types when that prevents invalid mixing and clarifies intent
- Prefer concrete structs over
map[string]any for stable data
- Keep weakly typed data at the boundary and translate it into strict internal types quickly
- Avoid boolean parameter soup; use named option structs or dedicated methods when intent is unclear
Comment Quality
Write comments when they add:
- why a tradeoff exists
- package-level intent
- non-obvious invariants or constraints
- concurrency ownership rules
- boundary assumptions
Do not write comments that:
- restate the code
- narrate obvious assignments
- explain syntax instead of intent
- leave vague TODOs without reason or ticket reference
- duplicate the doc comment with less precision
Documentation Discipline
- Exported names and packages need doc comments
- Public docs should describe contract, invariants, and caller-visible behavior
- When a change affects configuration, wire format, or API semantics, update docs in the same change
- Add or update examples when they materially improve discoverability of a public API
Test Quality
- Prefer readable subtest names over encoded case IDs
- Failure messages should make
got and want obvious
- Prefer semantic comparisons over formatting-sensitive comparisons
- Avoid asserting on exact human-readable error strings unless the exact string is part of the contract
- Use
t.Fatal only when the test cannot continue meaningfully
- Acceptance tests should speak in business behavior, not internal implementation vocabulary
- Use table-driven tests where variation is the point; do not force tables when a direct narrative test is clearer
- Add test seams instead of using sleeps, global mutation, or network reliance to force determinism
Static Analysis Discipline
Treat linting and static analysis as design feedback, not cosmetic cleanup.
- Respect repo gates for
go vet, golangci-lint, staticcheck, govulncheck, and related analyzers when configured
- Fix root causes instead of scattering ignores
- If an analyzer warning is intentionally ignored, leave a precise justification close to the suppression
- Do not weaken lint configuration casually to make a change pass
Review Checklist
Before finishing any change, verify:
Reject These Patterns
- Interface-per-struct without consumer need
- Giant functions mixing validation, orchestration, and persistence
- Hardcoded configuration or collaborator selection
- Weakly typed domain data kept as raw maps or generic blobs without need
- Comments that restate code
- Brittle mock-only tests — prefer fakes with real behavior
- Transport concerns embedded in core domain logic
- Production design distorted to satisfy a mocking framework
- Refactors that add indirection without improving correctness, ownership, or testability
Success Criteria
This skill is being followed correctly when:
- changes are small, test-backed, and easy to review
- dependency flow is explicit from the composition root
- package responsibilities are cleaner after the change, not blurrier
- the implementation follows the Go standards in
CLAUDE.md
- tests speak in behavior terms, not implementation vocabulary
- the resulting code reads clearly without comments explaining the control flow
1---2name: go-rig-33description: Use this skill when building, reviewing, or refactoring Go code that must follow strict design discipline — ATDD/TDD workflow, explicit dependency injection, package-boundary discipline, and structured code review. Complements CLAUDE.md by focusing on process and design judgment rather than version-specific Go features.4---56# Go Rig78Strict design and testing discipline for Go projects.910This skill **complements** `CLAUDE.md`.1112`CLAUDE.md` owns:13- Go version, toolchain, and commands14- Key style, error, context, and concurrency rules1516`.claude/rules/` owns:17- Go 1.26 idioms and go fix modernizer catalog (`go-idioms.md`)18- Detailed style, API, documentation, and testing patterns (`go-patterns.md`)1920This skill adds:21- ATDD/TDD workflow22- design principles and abstraction discipline23- dependency injection discipline24- package-boundary judgment25- documentation discipline26- comment quality standards27- structured review process2829Do not restate or override version-specific guidance from `CLAUDE.md`. If `CLAUDE.md` is stricter on a shared point, follow `CLAUDE.md`.3031## When to Use3233Use this skill when:3435- implementing a new feature or behavior increment36- refactoring Go code for clearer ownership or testability37- reviewing package boundaries or dependency flow38- replacing hidden collaborator construction with explicit injection39- tightening tests around user-visible or integration behavior4041## ATDD/TDD Workflow4243Test-first is a design tool, not an afterthought.44451. **Acceptance first** — define the boundary behavior before writing code462. **Acceptance test** — add or update an acceptance-level test if the project has that layer; otherwise express boundary behavior in the closest consumer-level test473. **Smallest failing unit test** — for the next behavior increment484. **Minimal implementation** — only enough to pass495. **Refactor** — improve readability and cohesion while green506. **Repeat** — next behavior increment5152If repository policy does not allow automatic test execution, still design test-first and ask before running.5354### Test Coverage Expectations5556Every meaningful change should cover:57- expected behavior (happy path)58- invalid input and validation failures59- edge cases and boundary values60- error and failure paths61- concurrency behavior when relevant6263Use the project’s existing test layers where possible. Reach for acceptance tests when the change is user-visible or integration-heavy, and unit tests when isolating business rules or edge cases.6465### Definition Of Done6667A change is not done when the code "works on one path." It is done when:68- acceptance behavior is specified at the right boundary69- the smallest relevant unit behavior is covered70- failure and edge behavior are covered71- code was refactored back to clarity after going green72- repo test/lint/static-analysis expectations were satisfied or explicitly deferred7374## Design Principles7576Apply without ceremony — these guide decisions, not generate boilerplate.7778**SRP** — each package, type, and function has one clear reason to change. Split when a change in one concern forces changes in an unrelated concern.7980**DRY** — extract repeated validation, mapping, branching, and business rules. Do not DRY away incidental similarity — two things that look alike but change for different reasons should stay separate.8182**OCP** — extend stable areas carefully, but do not invent indirection to satisfy the idea of extensibility. In Go, a concrete type with a small seam at the consumer is usually better than an abstract framework.8384When applying SRP/DRY/OCP in Go, prefer deleting duplication caused by mixed responsibilities before introducing new abstractions. The first move is usually better boundaries, not more interfaces.8586## Abstraction Discipline8788- Start with concrete types and direct calls89- Introduce an interface only when a real consumer needs substitution90- Prefer one seam at a boundary over many tiny abstractions in the core91- If an abstraction adds files, wiring, and names but no clear testability or ownership win, do not add it9293Avoid:94- interface-per-struct95- repositories or services that only forward calls96- configuration objects passed everywhere to avoid choosing explicit parameters97- “future-proofing” abstractions without a concrete second implementation or consumer9899## Function Design100101- A function should usually do one thing: validate, transform, orchestrate, persist, or render102- If a function mixes business rules with transport, storage, or logging details, split it103- Prefer early returns over nested condition pyramids104- Keep parameter lists explicit and intention-revealing; if many values travel together for one reason, introduce a small typed struct105- Use whitespace to separate logical phases so the control flow reads top to bottom106107Refactor when a function:108- needs comments to explain the control flow109- mixes unrelated reasons to change110- carries mutable state across many screens of code111- repeats branching or validation logic that belongs in a helper or type method112113## Dependency Injection114115- **Constructors** for types that must enforce invariants or own long-lived collaborators116- **Function parameters** for short-lived collaborators and pure logic117- Never construct DB clients, HTTP clients, loggers, or repositories inside domain methods118- No DI frameworks — explicit wiring only119- No hidden globals or singletons120- Prefer passing dependencies from the composition root (`main`, wiring package, or test setup) instead of looking them up deep inside the call stack121- Inject seams for time, randomness, process execution, filesystem, and external I/O when behavior depends on them122- Do not hide dependencies behind package-level variables except in rare compatibility shims123124```go125// constructor injection for long-lived deps126func NewOrderService(store OrderStore, clock Clock) *OrderService {127 return &OrderService{store: store, clock: clock}128}129130// function parameter for short-lived/pure logic131func ValidateOrder(order Order, now time.Time) error {132 if order.ExpiresAt.Before(now) {133 return fmt.Errorf("order %s expired: %w", order.ID, ErrExpired)134 }135 return nil136}137```138139## Package Design140141Organize by domain, not by technical layer.142143- Group related domain logic together until splitting clearly improves cohesion144- Keep transport and storage near the owning domain in the repo when the service is small, but do not let core business logic depend on transport details145- Split files when doing so improves readability; file count is not a goal by itself146- Split packages only when coupling pressure is real, not speculative147148Avoid:149- interface-per-struct without a consumer need150- deep layering in small services151- `internal/platform/` catch-all layers — keep cross-cutting concerns in focused packages (`internal/config/`, `internal/db/`)152- packages that combine unrelated domains because they share a datastore or transport153- "shared" packages that centralize unrelated helpers and create import gravity154155## Hardcoding And Configuration156157- Do not hardcode URLs, ports, credentials, file paths, timeouts, feature flags, environment names, or dependency selection in core logic158- Domain invariants may be constants, but operational values should come from config, constructor parameters, or function arguments159- Prefer typed config structs validated at startup over scattered `os.Getenv` calls160- Keep configuration loading at the edge; pass validated values inward161162## Type Discipline163164- Model domain concepts with named types when that prevents invalid mixing and clarifies intent165- Prefer concrete structs over `map[string]any` for stable data166- Keep weakly typed data at the boundary and translate it into strict internal types quickly167- Avoid boolean parameter soup; use named option structs or dedicated methods when intent is unclear168169## Comment Quality170171**Write comments when they add**:172- why a tradeoff exists173- package-level intent174- non-obvious invariants or constraints175- concurrency ownership rules176- boundary assumptions177178**Do not write comments that**:179- restate the code180- narrate obvious assignments181- explain syntax instead of intent182- leave vague TODOs without reason or ticket reference183- duplicate the doc comment with less precision184185## Documentation Discipline186187- Exported names and packages need doc comments188- Public docs should describe contract, invariants, and caller-visible behavior189- When a change affects configuration, wire format, or API semantics, update docs in the same change190- Add or update examples when they materially improve discoverability of a public API191192## Test Quality193194- Prefer readable subtest names over encoded case IDs195- Failure messages should make `got` and `want` obvious196- Prefer semantic comparisons over formatting-sensitive comparisons197- Avoid asserting on exact human-readable error strings unless the exact string is part of the contract198- Use `t.Fatal` only when the test cannot continue meaningfully199- Acceptance tests should speak in business behavior, not internal implementation vocabulary200- Use table-driven tests where variation is the point; do not force tables when a direct narrative test is clearer201- Add test seams instead of using sleeps, global mutation, or network reliance to force determinism202203## Static Analysis Discipline204205Treat linting and static analysis as design feedback, not cosmetic cleanup.206207- Respect repo gates for `go vet`, `golangci-lint`, `staticcheck`, `govulncheck`, and related analyzers when configured208- Fix root causes instead of scattering ignores209- If an analyzer warning is intentionally ignored, leave a precise justification close to the suppression210- Do not weaken lint configuration casually to make a change pass211212## Review Checklist213214Before finishing any change, verify:215216- [ ] Package boundaries are coherent — no cross-domain leaks217- [ ] No premature abstractions — interfaces have real consumers218- [ ] Dependencies injected explicitly — no hidden construction219- [ ] No hardcoded runtime values (URLs, ports, credentials, timeouts)220- [ ] Types are explicit where they protect domain correctness221- [ ] Functions are readable in one pass222- [ ] Functions do not mix unrelated responsibilities223- [ ] Repeated logic is unified only when it shares the same reason to change224- [ ] Errors wrapped with useful context (`%w`)225- [ ] Tests cover acceptance behavior and unit behavior226- [ ] TDD/ATDD flow was followed as closely as the repo constraints allowed227- [ ] Behavioral compatibility checked where public APIs, JSON, or persistence shape changed228- [ ] Nil vs empty behavior is intentional for slices, maps, pointers, and JSON fields229- [ ] Concurrency changes have a shutdown path and observable ownership230- [ ] Exported docs and package docs were updated when public behavior changed231- [ ] Tests are robust against irrelevant formatting churn232- [ ] Version/tooling guidance from `CLAUDE.md` and `.claude/rules/` has been followed233- [ ] Lint and test gates expected by the repo have been run or consciously deferred234235## Reject These Patterns236237- Interface-per-struct without consumer need238- Giant functions mixing validation, orchestration, and persistence239- Hardcoded configuration or collaborator selection240- Weakly typed domain data kept as raw maps or generic blobs without need241- Comments that restate code242- Brittle mock-only tests — prefer fakes with real behavior243- Transport concerns embedded in core domain logic244- Production design distorted to satisfy a mocking framework245- Refactors that add indirection without improving correctness, ownership, or testability246247## Success Criteria248249This skill is being followed correctly when:250251- changes are small, test-backed, and easy to review252- dependency flow is explicit from the composition root253- package responsibilities are cleaner after the change, not blurrier254- the implementation follows the Go standards in `CLAUDE.md`255- tests speak in behavior terms, not implementation vocabulary256- the resulting code reads clearly without comments explaining the control flow