Go's primary advantages are its fast compiler, standard toolchain, and language simplicity. The agent's greatest risks: unnecessary abstractions, goroutine/context mismanagement, and swallowed errors. Verification layers must focus precisely on these failure modes.
1. Recommended Stack
| Domain | Tooling |
|---|---|
| Formatting | gofumpt + goimports |
| Linting | golangci-lint v2 (gosec, errorlint, bodyclose, noctx, contextcheck, depguard, gocognit, sloglint, testifylint) |
| Testing | testing + testify (or go-cmp), table-driven, t.Parallel() |
| Races & Leaks | go test -race, go.uber.org/goleak |
| Integration Testing | testcontainers-go |
| Architecture Enforcement | go-arch-lint, depguard, internal/ package encapsulation |
| Mutation Testing | gremlins (alternative: go-mutesting) |
| Property & Fuzz Testing | Built-in go test -fuzz, pgregory.net/rapid |
| Security Auditing | govulncheck, gosec, gitleaks, Trivy |
| Code Generation | sqlc, oapi-codegen / buf (gRPC), mockery (strictly scoped) |
| Contract Testing | pact-go or OpenAPI + oapi-codegen (spec-first) |
| Code Coverage | go test -coverprofile + diff coverage verification |
2. Repository Structure
cmd/orders/main.go <- pure wiring, entry point only
internal/
domain/ <- pure Go domain logic, zero framework/DB imports
app/ <- use cases, port interfaces (consumer-defined)
adapters/
http/ <- HTTP handlers, DTOs
postgres/ <- sqlc + repository implementations
kafka/ <- event producers & consumers
platform/ <- shared configuration, logger, OpenTelemetry
api/openapi.yaml <- spec-first API contract
migrations/ <- immutable database migrations
docs/adr, docs/specs
.claude/skills, .claude/settings.json
AGENTS.md Makefile .golangci.yml .go-arch-lint.yml
The internal/ tree acts as the Go compiler's native architectural boundary. It serves as an inexpensive and robust barrier to constrain autonomous AI code generation.
3. AGENTS.md (Go Template)
# Orders Service
Go 1.23, chi, pgx/sqlc, PostgreSQL, slog.
## Core Commands
- Fast check: `make check` (fmt + vet + lint + test -race + arch)
- Complete test suite: `go test ./... -race -shuffle=on -count=1`
- Single package test: `go test ./internal/app/... -run TestPlaceOrder -v`
- Mutation testing: `make mutate`
- Code generation: `make generate` (sqlc, oapi-codegen); never edit generated files manually
## Design Rules
- The **consuming package defines interfaces**; never introduce "just in case" abstractions
- `context.Context` must always be the first parameter; never store context inside structs
- Wrap errors with causal context: `fmt.Errorf("...: %w", err)`; never swallow or discard with `_ =`
- Any code launching goroutines must own their full lifecycle (`context.Context`, `sync.WaitGroup`, `errgroup`)
- Reserve `panic` strictly for unrecoverable programmer defects; avoid `init()` functions and global state
- Use structured logging with `log/slog`; never use `fmt.Println` or `log.Print`
- The `domain` package must remain pure: zero imports of `net/http`, `database/sql`, or third-party frameworks
## Invariants & Prohibitions
- Never use `t.Skip`, delete test cases, or weaken assertion statements
- Do not introduce new third-party dependencies without human approval (`go get` requires sign-off)
- Never use `//nolint` directives without an explicit rationale comment
## Definition of Done
`make check` and `make mutate` (across domain/app) pass cleanly; all spec acceptance criteria are backed by tests.
4. Makefile (Single Verification Entry Point)
.PHONY: check fmt lint test arch vuln mutate generate
check: fmt lint test arch
fmt:
gofumpt -l -w . && goimports -w .
lint:
golangci-lint run ./...
test:
go test ./... -race -shuffle=on -count=1 -coverprofile=cover.out
arch:
go-arch-lint check
vuln:
govulncheck ./...
mutate:
gremlins unleash --threshold-efficacy 70 --threshold-mcover 60 ./internal/domain/... ./internal/app/...
generate:
go generate ./...
5. golangci-lint (v2) Configuration
Configure .golangci.yml to catch anti-patterns before code review:
version: "2"
run:
timeout: 5m
linters:
default: standard # errcheck, govet, ineffassign, staticcheck, unused
enable:
- gosec
- errorlint
- bodyclose
- noctx
- contextcheck
- gocognit
- revive
- sloglint
- testifylint
- tparallel
- depguard
- forbidigo
- nilerr
- exhaustive
settings:
gocognit: { min-complexity: 15 }
forbidigo:
forbid:
- pattern: ^(fmt\.Print(f|ln)?|print|println)$
msg: Use structured slog logging instead
depguard:
rules:
domain:
files: ["**/internal/domain/**"]
deny:
- { pkg: net/http, desc: "Domain must not import HTTP packages" }
- { pkg: database/sql, desc: "Domain must not import database/sql" }
- { pkg: github.com/jackc/pgx, desc: "Domain must not import pgx drivers" }
- { pkg: github.com/go-chi/chi, desc: "Domain must not import web router frameworks" }
formatters:
enable: [gofumpt, goimports]
issues:
max-issues-per-linter: 0
max-same-issues: 0
Enforce mandatory rationale on lint suppressions using nolintlint. Unconstrained agents will naturally suppress linter warnings rather than resolving architectural root causes.
6. Architecture Fitness Testing (go-arch-lint)
Define .go-arch-lint.yml to enforce architectural boundaries deterministically:
version: 3
workdir: internal
allow:
depOnAnyVendor: false # Vendor imports must be explicitly enabled per component
components:
domain: { in: domain/** }
app: { in: app/** }
http: { in: adapters/http/** }
postgres: { in: adapters/postgres/** }
platform: { in: platform/** }
deps:
domain: {} # Zero incoming and outgoing layer dependencies
app: { mayDependOn: [domain] }
http: { mayDependOn: [app, domain, platform], canUse: [chi, oapi] }
postgres: { mayDependOn: [app, domain, platform], canUse: [pgx] }
platform: { anyVendorDeps: true }
vendors:
chi: { in: github.com/go-chi/chi/v5 }
oapi: { in: github.com/oapi-codegen/runtime }
pgx: { in: github.com/jackc/pgx/v5 }
Go provides fundamental architectural protections by default:
- Circular imports are strictly prohibited by the compiler at zero compute cost.
- For lightweight package boundary verification without external tools, use standard
go/packagestests:
func TestDomainDoesNotImportHTTP(t *testing.T) {
pkgs, err := packages.Load(&packages.Config{Mode: packages.NeedImports | packages.NeedName},
"./internal/domain/...")
require.NoError(t, err)
for _, p := range pkgs {
for path := range p.Imports {
require.NotContains(t, path, "net/http", "%s must not import net/http", p.PkgPath)
}
}
}
arch-go can also be used as an alternative, defining strict rules on dependencies, maximum function length, naming conventions, and structural contents (e.g., restricting domain packages to interfaces and structs).
7. Mutation Testing (gremlins)
go install github.com/go-gremlins/gremlins/cmd/gremlins@latest
gremlins unleash --threshold-efficacy 70 --threshold-mcover 60 ./internal/domain/... ./internal/app/...
- Efficacy Ratio: Killed mutants / (Killed mutants + Survived mutants). Maintain target efficacy > 70–80% in domain layers.
- Restrict mutation runs strictly to
internal/domainandinternal/app. Running mutation analysis on transport adapters or generated code generates excessive noise and execution latency. - When running in CI, constrain scope with
--workersand run incremental checks on changed packages during PR reviews, reserving full suite sweeps for nightly pipelines. - Exclude generated files (such as
*_gen.goand sqlc outputs) in the tool'sexclude-filesconfiguration. - Route surviving mutants directly to an autonomous triage subagent to author missing test cases.
- Acknowledge equivalent mutants pragmatically and elevate coverage thresholds incrementally.
8. Testing Practices
Table-Driven Tests, Concurrency & Goroutine Leak Detection
func TestMain(m *testing.M) { goleak.VerifyTestMain(m) }
func TestDiscount(t *testing.T) {
t.Parallel()
tests := []struct {
name string
price, rate int
want int
wantErr error
}{
{"10 percent discount", 1000, 10, 900, nil},
{"zero discount", 0, 10, 0, nil},
{"invalid rate", 1000, 120, 0, ErrInvalidDiscountRate},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, err := Discount(tt.price, tt.rate)
require.ErrorIs(t, err, tt.wantErr)
require.Equal(t, tt.want, got)
})
}
}
- Always enable
-race: AI agents frequently write subtle data races under concurrent flows. - Run tests with
-shuffle=on: Uncover hidden test execution order dependencies early. - Fuzzing for inputs, parsing, and serialization:
func FuzzParseSKU(f *testing.F) {
f.Add("ABC-123")
f.Fuzz(func(t *testing.T, s string) {
sku, err := ParseSKU(s)
if err == nil {
require.Equal(t, s, sku.String()) // Round-trip invariant verification
}
})
}
- Property-Based Testing: Use
pgregory.net/rapidto verify invariants (sorting consistency, total sums, idempotency). - Integration Testing: Use
testcontainers-gofor actual PostgreSQL instances; usenet/http/httptestfor handlers. - Time and Determinism: Inject a
clockinterface or atime.Nowprovider function. Bantime.Sleepsynchronization in tests. - Fakes over Mocking: Keep interfaces minimal. A handwritten in-memory struct fake is cleaner and more refactor-resilient than generated mocks.
- Golden Files: Store complex JSON snapshots in
testdata/and update them explicitly using a-updateflag. - Benchmark Regressions: Use
benchstatto prevent performance regressions on critical execution paths in CI.
9. Claude Code Integration Hooks
Automate guardrails locally using .claude/settings.json:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": ".claude/hooks/guard-bash.sh" }]
}],
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": ".claude/hooks/post-edit.sh" }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": ".claude/hooks/on-stop.sh" }]
}]
}
}
.claude/hooks/post-edit.sh (Format, vet, and prohibit test skipping):
#!/usr/bin/env bash
f=$(jq -r '.tool_input.file_path' < /dev/stdin)
case "$f" in
*_gen.go|*.sql.go) echo "Generated file: run make generate instead" >&2; exit 2 ;;
*.go)
gofumpt -w "$f" && goimports -w "$f"
go vet "$(dirname "$f")/..." 2>&1 >&2 || exit 2 ;;
esac
if grep -Eq 't\.Skip\(|//nolint\s*$' "$f" 2>/dev/null; then
echo "t.Skip or unannotated //nolint directives are forbidden" >&2; exit 2
fi
.claude/hooks/guard-bash.sh (Block destructive and unapproved commands):
#!/usr/bin/env bash
cmd=$(jq -r '.tool_input.command' < /dev/stdin)
echo "$cmd" | grep -Eq 'git push.*--force|rm -rf /|go get |DROP TABLE' \
&& { echo "Blocked: $cmd (dependencies and schema drops require human confirmation)" >&2; exit 2; }
exit 0
.claude/hooks/on-stop.sh (Prevent premature agent termination):
#!/usr/bin/env bash
make check 2>&1 | tail -40 >&2 || exit 2
10. Skill Templates
.claude/skills/add-usecase/SKILL.md:
---
name: add-usecase
description: Use when adding a new use case. Generates domain, app, adapter, and tests following hexagonal architecture.
---
1. Read docs/specs and api/openapi.yaml; extract acceptance criteria.
2. Author failing table-driven tests first (domain/app).
3. Define the port interface inside the app package (consuming side); implement the adapter.
4. Wrap errors with contextual information (%w), declare sentinel/typed errors, and ensure context is the first parameter.
5. Run `make generate` if applicable; never modify generated code manually.
6. Verify `make check` is fully green before finishing. Keep commits atomic: test, implementation, adapter.
.claude/skills/concurrency-review/SKILL.md:
---
name: concurrency-review
description: Review code that introduces goroutines, channels, sync primitives, or context handling.
---
Verification Checklist:
- How does the goroutine terminate? Is context cancellation or errgroup utilized?
- Who is responsible for closing the channel?
- Are loop variables captured correctly?
- Is shared state protected by a mutex or race-free communication?
- Is the test suite executing with `go test -race`?
- Is `goleak.VerifyTestMain` configured?
Report findings with file and line references. Do not modify source files during review.
Recommended additional skills: mutation-triage, sqlc-migration, openapi-first, add-arch-rule, and security-review.
11. Continuous Integration Pipeline (GitHub Actions)
name: ci
on: [pull_request]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-go@v5
with: { go-version-file: go.mod, cache: true }
- name: Verify generated code is up-to-date
run: make generate && git diff --exit-code
- uses: golangci/golangci-lint-action@v7
- run: go test ./... -race -shuffle=on -count=1 -coverprofile=cover.out
- run: go install github.com/fe3dback/go-arch-lint@latest && go-arch-lint check
- run: go install golang.org/x/vuln/cmd/govulncheck@latest && govulncheck ./...
- run: go mod tidy && git diff --exit-code go.mod go.sum
- run: make mutate # Or restrict to packages affected in the PR
- uses: gitleaks/gitleaks-action@v2
Nightly pipelines execute comprehensive mutation suites, long-running fuzzing (-fuzztime=5m), benchmark regression comparisons, and dependency vulnerability checks (go list -m -u all). Branch protection must require all verify jobs to pass before merge.
12. Go-Specific AI Pitfalls and Remedies
| AI Pitfall | Automated Remedy |
|---|---|
Swallowed or discarded errors (_ = f()) | errcheck, errorlint, nilerr |
Returning bare errors without context, raw == checks | errorlint, %w wrapping, errors.Is and errors.As |
| Loop variable capture, goroutine lifecycle leaks | Go 1.22+ loop semantics, go.uber.org/goleak, -race |
Lost context, unbounded context.Background() propagation | contextcheck, noctx, explicit AGENTS.md rules |
| Over-engineered interfaces, premature Java-style abstraction | "Consuming package defines interfaces" rule, peer review |
Global mutable state, unnecessary init() functions | gochecknoinits, gochecknoglobals in linter config |
Improper defer in loops, unclosed resp.Body | bodyclose, revive, architectural review |
| Slice and map aliasing, writing to nil maps | Unit testing + fuzzing + race detector |
time.Sleep used for test synchronization | Strict prohibition in guidelines; use channels, polling with timeout, or fake clocks |
| Hallucinated third-party dependencies, outdated package APIs | Human approval for go get, govulncheck, go mod tidy diff checks in CI |
Silencing linter diagnostics with bare //nolint | nolintlint requiring explicit explanation, post-edit hooks |
| Manual edits to generated code (sqlc, protobuf) | Pre-commit hooks + CI check (make generate && git diff --exit-code) |
| Raw SQL string concatenation | sqlc parameterized queries, gosec static checks |
13. Phased Adoption Roadmap
- Foundation: Establish
AGENTS.md,Makefile,golangci-lint,gofumpt, and mandatory-raceflag across all test invocations. - Architecture: Structure repository boundaries with
internal/,depguard, andgo-arch-lint. - Continuous Verification: Integrate
govulncheck,gitleaks, generated code diff detection, andgo mod tidyassertions into CI. - Testing Discipline: Standardize table-driven patterns,
goleak,testcontainers-go, and eliminate flaky sleeps. - Mutation Analysis: Deploy
gremlinsoninternal/domainandinternal/app, stepping efficacy thresholds from 60% toward 75%. - Agent Ergonomics: Configure local tooling hooks, tailored delivery skills, and dedicated reviewer subagents.
- Advanced Quality Gates: Introduce property-based testing with
rapid, native fuzzing,benchstatperformance assertions, and spec-first OpenAPI verification.
Netology