Home / Publications / 2026

// Best Practice Guide

AI-Assisted Delivery: Go Edition

Hasan Ozgan

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

DomainTooling
Formattinggofumpt + goimports
Lintinggolangci-lint v2 (gosec, errorlint, bodyclose, noctx, contextcheck, depguard, gocognit, sloglint, testifylint)
Testingtesting + testify (or go-cmp), table-driven, t.Parallel()
Races & Leaksgo test -race, go.uber.org/goleak
Integration Testingtestcontainers-go
Architecture Enforcementgo-arch-lint, depguard, internal/ package encapsulation
Mutation Testinggremlins (alternative: go-mutesting)
Property & Fuzz TestingBuilt-in go test -fuzz, pgregory.net/rapid
Security Auditinggovulncheck, gosec, gitleaks, Trivy
Code Generationsqlc, oapi-codegen / buf (gRPC), mockery (strictly scoped)
Contract Testingpact-go or OpenAPI + oapi-codegen (spec-first)
Code Coveragego 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/packages tests:
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/domain and internal/app. Running mutation analysis on transport adapters or generated code generates excessive noise and execution latency.
  • When running in CI, constrain scope with --workers and run incremental checks on changed packages during PR reviews, reserving full suite sweeps for nightly pipelines.
  • Exclude generated files (such as *_gen.go and sqlc outputs) in the tool's exclude-files configuration.
  • 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/rapid to verify invariants (sorting consistency, total sums, idempotency).
  • Integration Testing: Use testcontainers-go for actual PostgreSQL instances; use net/http/httptest for handlers.
  • Time and Determinism: Inject a clock interface or a time.Now provider function. Ban time.Sleep synchronization 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 -update flag.
  • Benchmark Regressions: Use benchstat to 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 PitfallAutomated Remedy
Swallowed or discarded errors (_ = f())errcheck, errorlint, nilerr
Returning bare errors without context, raw == checkserrorlint, %w wrapping, errors.Is and errors.As
Loop variable capture, goroutine lifecycle leaksGo 1.22+ loop semantics, go.uber.org/goleak, -race
Lost context, unbounded context.Background() propagationcontextcheck, noctx, explicit AGENTS.md rules
Over-engineered interfaces, premature Java-style abstraction"Consuming package defines interfaces" rule, peer review
Global mutable state, unnecessary init() functionsgochecknoinits, gochecknoglobals in linter config
Improper defer in loops, unclosed resp.Bodybodyclose, revive, architectural review
Slice and map aliasing, writing to nil mapsUnit testing + fuzzing + race detector
time.Sleep used for test synchronizationStrict prohibition in guidelines; use channels, polling with timeout, or fake clocks
Hallucinated third-party dependencies, outdated package APIsHuman approval for go get, govulncheck, go mod tidy diff checks in CI
Silencing linter diagnostics with bare //nolintnolintlint 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 concatenationsqlc parameterized queries, gosec static checks

13. Phased Adoption Roadmap

  1. Foundation: Establish AGENTS.md, Makefile, golangci-lint, gofumpt, and mandatory -race flag across all test invocations.
  2. Architecture: Structure repository boundaries with internal/, depguard, and go-arch-lint.
  3. Continuous Verification: Integrate govulncheck, gitleaks, generated code diff detection, and go mod tidy assertions into CI.
  4. Testing Discipline: Standardize table-driven patterns, goleak, testcontainers-go, and eliminate flaky sleeps.
  5. Mutation Analysis: Deploy gremlins on internal/domain and internal/app, stepping efficacy thresholds from 60% toward 75%.
  6. Agent Ergonomics: Configure local tooling hooks, tailored delivery skills, and dedicated reviewer subagents.
  7. Advanced Quality Gates: Introduce property-based testing with rapid, native fuzzing, benchstat performance assertions, and spec-first OpenAPI verification.

Related publications