{"id":"b9b1770f-f0e9-4172-a9f4-d6e21dd357ae","entityType":"agent","slug":"clawhub-tenequm-go-dev","name":"go-dev","canonicalUrl":"https://www.xpersona.co/agent/clawhub-tenequm-go-dev","canonicalPath":"/agent/clawhub-tenequm-go-dev","generatedAt":"2026-10-10T08:44:05.643Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":null},"description":"Opinionated Go setup with golangci-lint v2, gofumpt, gotestsum, golang-migrate, and just. Use when starting a Go project, configuring lint, format, test, coverage or CI, writing a Justfile, wiring migrations, or leaving a Makefile workflow.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.7K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:go-dev","sourceUrl":"https://clawhub.ai/tenequm/go-dev","homepage":"https://clawhub.ai/tenequm/skills/go-dev","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/tenequm/go-dev","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/tenequm/skills/go-dev","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"go-dev technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":null},"stars":null,"forks":null,"downloads":1708,"packageName":null,"latestVersion":"0.6.0","tractionLabel":"1.7K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T03:58:51.269Z","lastCrawledAt":"2026-10-10T03:58:51.269Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T03:58:51.269Z","lastVerifiedAt":null,"highlights":[{"version":"0.6.0","createdAt":"2026-10-06T12:07:45.096Z","changelog":"Updated go-dev from 0.5.0 to 0.6.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/go-testing-reference.md` - modified `references/gofumpt-reference.md` - modified `references/golangci-lint-reference.md` - modified `references/gotestsum-reference.md` - modified `references/justfile-reference.md` - modified `references/lefthook-reference.md`","fileCount":12,"zipByteSize":75742},{"version":"0.5.0","createdAt":"2026-09-30T22:24:54.200Z","changelog":"Updated go-dev from 0.4.1 to 0.5.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/go-migrate-reference.md` - modified `references/go-testing-reference.md` - modified `references/gofumpt-reference.md` - modified `references/golangci-lint-reference.md` - modified `references/gotestsum-reference.md` - modified `references/justfile-reference.md` - modified `references/lefthook-reference.md`","fileCount":12,"zipByteSize":71080},{"version":"0.4.1","createdAt":"2026-09-17T20:29:39.983Z","changelog":"Updated go-dev from 0.4.0 to 0.4.1. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/lefthook-reference.md`","fileCount":12,"zipByteSize":60203},{"version":"0.4.0","createdAt":"2026-09-09T12:10:54.745Z","changelog":"Updated go-dev from 0.3.1 to 0.4.0. Changes: - modified `CHANGELOG.md` - modified `SKILL.md` - modified `references/go-migrate-reference.md` - modified `references/go-testing-reference.md` - modified `references/gofumpt-reference.md` - modified `references/golangci-lint-reference.md` - modified `references/gotestsum-reference.md` - modified `references/justfile-reference.md` - added `references/lefthook-reference.md`","fileCount":12,"zipByteSize":58252},{"version":"0.3.1","createdAt":"2026-09-09T09:55:39.899Z","changelog":"Updated go-dev from 0.3.0 to 0.3.1. Changes: - modified `CHANGELOG.md` - modified `SKILL.md`","fileCount":11,"zipByteSize":45013},{"version":"0.3.0","createdAt":"2026-08-26T10:19:50.184Z","changelog":"Updated go-dev from 0.2.4 to 0.3.0. Changes: - added `CHANGELOG.md` - modified `SKILL.md` - modified `references/go-migrate-reference.md` - modified `references/go-testing-reference.md` - modified `references/gofumpt-reference.md` - modified `references/golangci-lint-reference.md` - modified `references/gotestsum-reference.md` - modified `references/justfile-reference.md`","fileCount":11,"zipByteSize":44817},{"version":"0.2.4","createdAt":"2026-08-21T12:01:19.748Z","changelog":"Updated go-dev from 0.2.3 to 0.2.4. Changes: - modified `SKILL.md` - deleted `skill-card.md`","fileCount":10,"zipByteSize":33123},{"version":"0.2.3","createdAt":"2026-08-07T13:36:09.740Z","changelog":"Updated go-dev from 0.2.2 to 0.2.3. Changes: - modified `SKILL.md` - modified `skill-card.md`","fileCount":10,"zipByteSize":33083}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:go-dev","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17bp3v1hm1dnkzey0c9tfh02183j0y5:go-dev` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/tenequm/go-dev before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T08:44:05.640Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-tenequm-go-dev/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":null},"readme":"Skill: go-dev\n\nOwner: tenequm\n\nSummary: Opinionated Go setup with golangci-lint v2, gofumpt, gotestsum, golang-migrate, and just. Use when starting a Go project, configuring lint, format, test, coverage or CI, writing a Justfile, wiring migrations, or leaving a Makefile workflow.\n\nTags: latest:0.6.0\n\nVersion history:\n\nv0.6.0 | 2026-10-06T12:07:45.096Z | user\n\nUpdated go-dev from 0.5.0 to 0.6.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/go-testing-reference.md`\n- modified `references/gofumpt-reference.md`\n- modified `references/golangci-lint-reference.md`\n- modified `references/gotestsum-reference.md`\n- modified `references/justfile-reference.md`\n- modified `references/lefthook-reference.md`\n\nv0.5.0 | 2026-09-30T22:24:54.200Z | user\n\nUpdated go-dev from 0.4.1 to 0.5.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/go-migrate-reference.md`\n- modified `references/go-testing-reference.md`\n- modified `references/gofumpt-reference.md`\n- modified `references/golangci-lint-reference.md`\n- modified `references/gotestsum-reference.md`\n- modified `references/justfile-reference.md`\n- modified `references/lefthook-reference.md`\n\nv0.4.1 | 2026-09-17T20:29:39.983Z | user\n\nUpdated go-dev from 0.4.0 to 0.4.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/lefthook-reference.md`\n\nv0.4.0 | 2026-09-09T12:10:54.745Z | user\n\nUpdated go-dev from 0.3.1 to 0.4.0.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/go-migrate-reference.md`\n- modified `references/go-testing-reference.md`\n- modified `references/gofumpt-reference.md`\n- modified `references/golangci-lint-reference.md`\n- modified `references/gotestsum-reference.md`\n- modified `references/justfile-reference.md`\n- added `references/lefthook-reference.md`\n\nv0.3.1 | 2026-09-09T09:55:39.899Z | user\n\nUpdated go-dev from 0.3.0 to 0.3.1.\nChanges:\n- modified `CHANGELOG.md`\n- modified `SKILL.md`\n\nv0.3.0 | 2026-08-26T10:19:50.184Z | user\n\nUpdated go-dev from 0.2.4 to 0.3.0.\nChanges:\n- added `CHANGELOG.md`\n- modified `SKILL.md`\n- modified `references/go-migrate-reference.md`\n- modified `references/go-testing-reference.md`\n- modified `references/gofumpt-reference.md`\n- modified `references/golangci-lint-reference.md`\n- modified `references/gotestsum-reference.md`\n- modified `references/justfile-reference.md`\n\nv0.2.4 | 2026-08-21T12:01:19.748Z | user\n\nUpdated go-dev from 0.2.3 to 0.2.4.\nChanges:\n- modified `SKILL.md`\n- deleted `skill-card.md`\n\nv0.2.3 | 2026-08-07T13:36:09.740Z | user\n\nUpdated go-dev from 0.2.2 to 0.2.3.\nChanges:\n- modified `SKILL.md`\n- modified `skill-card.md`\n\nv0.2.2 | 2026-07-22T18:44:11.714Z | user\n\nUpdated go-dev from 0.2.1 to 0.2.2.\nChanges:\n- modified `SKILL.md`\n- added `skill-card.md`\n\nv0.2.1 | 2026-04-30T18:04:19.964Z | user\n\nUpdated go-dev from 0.2.0 to 0.2.1.\nChanges:\n- modified `SKILL.md`\n\nv0.2.0 | 2026-04-29T17:42:40.888Z | user\n\nInitial publish of go-dev 0.2.0.\nChanges:\n- added `LICENSE.txt`\n- added `SKILL.md`\n- added `references/go-migrate-reference.md`\n- added `references/go-testing-reference.md`\n- added `references/gofumpt-reference.md`\n- added `references/golangci-lint-reference.md`\n- added `references/gotestsum-reference.md`\n- added `references/justfile-reference.md`\n\nArchive index:\n\nArchive v0.6.0: 12 files, 75742 bytes\n\nFiles: CHANGELOG.md (15598b), LICENSE.txt (9157b), references/go-migrate-reference.md (13086b), references/go-testing-reference.md (23170b), references/gofumpt-reference.md (11676b), references/golangci-lint-reference.md (30886b), references/gotestsum-reference.md (9500b), references/justfile-reference.md (17920b), references/lefthook-reference.md (18701b), skill-card.md (2578b), SKILL.md (31081b), _meta.json (125b)\n\nFile v0.6.0:SKILL.md\n\n---\nname: go-dev\ndescription: Opinionated Go setup with golangci-lint v2, gofumpt, gotestsum, golang-migrate, and just. Use when starting a Go project, configuring lint, format, test, coverage or CI, writing a Justfile, wiring migrations, or leaving a Makefile workflow.\nmetadata:\n  version: \"0.6.0\"\n  categories: \"development\"\n  topics: \"go, golangci-lint, gofumpt, testing, just\"\n  upstream: \"go@1.27.1, golangci-lint@v2.14.0, gofumpt@v0.12.0, gotestsum@v1.13.0, golang-migrate@v4.20.1, just@1.58.0, lefthook@v2.1.17\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/go-dev\n    emoji: \"🐹\"\n    envVars:\n      - name: DATABASE_URL\n        required: false\n        description: Connection string used by the Justfile migration recipes (golang-migrate)\n---\n\n# Go Development Stack\n\nOpinionated, modern Go development setup. One tool per concern, zero overlap.\n\n## When to Use\n\n- Starting a new Go project from scratch\n- Adding linting, formatting, or testing infrastructure\n- Setting up CI/CD for a Go service or library\n- Creating a Justfile to replace a Makefile\n- Adding database migration tooling\n- Migrating from scattered gofmt/govet/staticcheck invocations to a unified setup\n\nNot for a fork that regularly merges from upstream: replacing its Makefile and reformatting the tree with gofumpt conflicts on every merge and buries real changes in a reformat diff. Keep upstream's gofmt and build tooling there, and only add checks.\n\n## The Stack\n\n| Tool | Version | Role | Replaces |\n|------|---------|------|----------|\n| **Go** | 1.27+ | Language, toolchain, `go mod`, `go fix` | - |\n| **golangci-lint** | v2.14+ | Meta-linter (100+ linters + formatters + `fmt` command) | gofmt, govet, staticcheck, errcheck run separately |\n| **gofumpt** | v0.12+ | Strict formatter (superset of gofmt, 19 default rules) | gofmt |\n| **gotestsum** | v1.13+ | Test runner with readable output, watch mode, JUnit XML | Raw `go test` |\n| **just** | 1.58+ | Task runner | Makefile |\n| **golang-migrate** | v4.20+ | DB migrations (CLI + library + `embed.FS`) | Manual SQL scripts |\n| **lefthook** | v2.1+ | Git hooks (single binary, parallel) | pre-commit (Python) |\n\n**Version floors are load-bearing.** golangci-lint \"supports Go versions lower or equal to the Go version used to compile it\" - a pin older than your Go toolchain fails outright. Go 1.27 support landed in golangci-lint v2.13.0, so `v2.13` is the floor for a Go 1.27 project; this skill pins v2.14.0, the first release that bundles gofumpt v0.12.0. Two more floors moved recently: gofumpt v0.12.0 \"is based on Go 1.27's gofmt, and requires Go 1.26 or later\", and lefthook's `go install` path now asks for Go 1.26+.\n\n## Quick Start: New Project\n\n```bash\n# 1. Create module\nmkdir myapp && cd myapp\ngo mod init github.com/yourorg/myapp\n\n# 2. Scaffold directories\nmkdir -p cmd/myapp internal migrations\n\n# 3. Install golangci-lint as a binary, not as a module tool (see note below)\ncurl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0\n\n# 4. Track the rest in go.mod (Go 1.24+ tool directive). Pin versions - never @latest,\n#    which recompiles the tool on every CI run and drifts between machines.\ngo get -tool mvdan.cc/gofumpt@v0.12.0\ngo get -tool gotest.tools/gotestsum@v1.13.0\n\n# golang-migrate needs a build tag, so install it directly\ngo install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.20.1\n\n# 5. Create config files (templates below)\n# 6. Run: just check\n```\n\n**Do not install golangci-lint through the tools pattern.** Upstream is explicit: \"Using `go install`/`go get`, \\\"tools pattern\\\", and `tool` command/directives installations aren't guaranteed to work. We recommend using binary installation.\" The reason that matters in a shared repo is dependency bleed - \"the dependencies of a tool can modify the dependencies of another tool or your project\". If you must have it in `go.mod`, isolate it behind its own `-modfile` - see the [golangci-lint Reference](references/golangci-lint-reference.md).\n\n**`go get -tool` tracks; `go tool` runs.** The tool directive records the dependency in `go.mod` but puts nothing on your PATH. Either invoke through the toolchain - `go tool gofumpt -l .`, `go tool gotestsum --format testname` - or `go install tool` once to populate `$(go env GOPATH)/bin`. The Justfile below calls the bare binaries, so it assumes the `go install tool` route (or a system install via Homebrew - but \"Homebrew can use an unexpected version of Go to build the binary\" for golangci-lint, and it cannot pin a version). Note that `go tool` resolves against the module in the current directory - \"additional tools may be defined in the go.mod of the current module\" - so in a monorepo it fails with `go: no such tool \"...\"` unless the recipe sets `[working-directory(...)]`.\n\nTwo Go-command behaviours worth knowing before the first commit:\n\n- `go mod init` under a 1.N toolchain writes `go 1.(N-1).0`, not `1.N` - \"Running `go mod init` using a toolchain of version `1.N.X` will create a `go.mod` file specifying the Go version `go 1.(N-1).0`.\" Bump the directive deliberately if you want 1.N language features.\n- Set a `toolchain go1.27.1` line in `go.mod`, but know it is a floor, not a pin: under the default `GOTOOLCHAIN=auto` the `go` command switches only \"if ... `<tname>` is newer than the default Go toolchain\", so a newer local `go` wins. For an exact toolchain set `GOTOOLCHAIN=go1.27.1` (CI or `go env -w`); `GODEBUG=toolchaintrace=1 go version` shows which one was picked. Keep the line at the current patch, not the `.0`: it selects the `go` that `govulncheck` scans stdlib advisories against on any machine whose own `go` is older, so a stale patch red-lights CI on its own - see Footguns below.\n\n## .golangci.yml\n\n```yaml\nversion: \"2\"\n\nrun:\n  timeout: 5m\n  build-tags:\n    - integration   # otherwise files behind the Justfile's integration tag are never linted\n\nlinters:\n  default: standard\n  enable:\n    - bodyclose\n    - copyloopvar\n    - dupl\n    - durationcheck\n    - err113\n    - errname\n    - errorlint\n    - exhaustive\n    - exptostd\n    - fatcontext\n    - goconst\n    - gocritic\n    - gosec\n    - intrange\n    - misspell\n    - modernize\n    - musttag\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - nonamedreturns\n    - perfsprint\n    - prealloc\n    - revive\n    - sqlclosecheck\n    - testifylint\n    - thelper\n    - unconvert\n    - unparam\n    - usestdlibvars\n    - usetesting\n    - wastedassign\n    - whitespace\n    - wrapcheck\n  settings:\n    govet:\n      enable:\n        - shadow\n    gocritic:\n      enabled-checks:\n        - nestingReduce\n    revive:\n      enable-all-rules: true\n      rules:\n        # enable-all-rules turns on `unhandled-error`, which flags `fmt.Println` in main.\n        # Under enable-all-rules a rule's `arguments` are ignored (the rule registers\n        # twice), so an allowlist does not work here - only `disabled` takes effect.\n        - name: unhandled-error\n          disabled: true\n    errcheck:\n      check-type-assertions: true\n  exclusions:\n    generated: strict\n    presets:\n      - comments\n      - std-error-handling\n      - common-false-positives\n    rules:\n      - path: _test\\.go\n        linters:\n          - errcheck\n          - dupl\n          - gosec\n          - wrapcheck\n\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  settings:\n    gofumpt:\n      # Select rules individually. `extra-rules: true` is deprecated, and it also\n      # switches on `balance_calls`, which gofumpt itself demoted as controversial.\n      extra:\n        group-params: true\n        clothe-returns: true\n        balance-calls: false\n  exclusions:\n    generated: strict\n    paths:\n      - vendor/\n\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      colors: true\n  sort-order:\n    - linter\n    - file\n  show-stats: true\n```\n\n## Justfile\n\n```just\nset shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]\nset dotenv-load\n\nbinary := \"myapp\"\n\n[private]\ndefault:\n    @just --list --unsorted\n\n# ── Code Quality ──────────────────────────────────────────\n\n# Format all Go code\n[group('quality')]\nfmt:\n    golangci-lint fmt ./...\n\n# Check formatting without modifying (CI-safe)\n[group('quality')]\nfmt-check:\n    golangci-lint fmt --diff ./...\n\n# Run linter\n[group('quality')]\nlint:\n    golangci-lint run ./...\n\n# Run linter with auto-fix\n[group('quality')]\nlint-fix:\n    golangci-lint run --fix ./...\n\n# Run vulnerability check\n[group('quality')]\nvuln:\n    govulncheck ./...\n\n# ── Testing ───────────────────────────────────────────────\n\n# Run all tests with race detection\n[group('test')]\ntest *args=\"./...\":\n    gotestsum --format testname -- -race {{ args }}\n\n# Run tests with coverage\n[group('test')]\ntest-cov:\n    gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...\n    go tool cover -func=coverage.out\n\n# Open coverage report in browser\n[group('test')]\ncoverage: test-cov\n    go tool cover -html=coverage.out\n\n# Run integration tests\n[group('test')]\ntest-integration:\n    gotestsum --format testname -- -race -tags=integration ./...\n\n# Watch tests during development\n[group('test')]\ntest-watch:\n    gotestsum --watch --watch-clear --format testname\n\n# Run benchmarks\n[group('test')]\nbench:\n    go test -bench=. -benchmem ./...\n\n# ── Build ─────────────────────────────────────────────────\n\n# Build the binary\n[group('build')]\nbuild:\n    go build -o {{ binary }} ./cmd/{{ binary }}\n\n# Build optimized release binary\n[group('build')]\nbuild-release:\n    CGO_ENABLED=0 go build -trimpath -ldflags=\"-s -w\" -o {{ binary }} ./cmd/{{ binary }}\n\n# ── Dependencies ──────────────────────────────────────────\n\n# Tidy and verify modules\n[group('deps')]\ntidy:\n    go mod tidy\n    go mod verify\n\n# Fail if go.mod/go.sum are untidy, without touching them (CI-safe)\n[group('deps')]\ntidy-check:\n    go mod tidy -diff\n\n# Run code generators\n[group('deps')]\ngenerate:\n    go generate ./...\n\n# ── Database ──────────────────────────────────────────────\n\n# Apply all pending migrations\n[group('db')]\nmigrate-up:\n    migrate -path migrations -database \"$DATABASE_URL\" up\n\n# Revert last migration\n[group('db')]\nmigrate-down:\n    migrate -path migrations -database \"$DATABASE_URL\" down 1\n\n# Create a new migration\n[group('db')]\nmigrate-create name:\n    migrate create -ext sql -dir migrations -seq {{ name }}\n\n# ── CI ────────────────────────────────────────────────────\n\n# Full CI gate (format check + lint + test)\n[group('ci')]\ncheck: fmt-check lint test\n    @echo \"All checks passed\"\n\n# Clean build artifacts\n[group('ci')]\nclean:\n    go clean\n    rm -f {{ binary }} coverage.out\n```\n\n`check` deliberately leaves out `vuln`: govulncheck calls the network, and a vulnerability-database update can turn the gate red on a change that touched nothing. Run it as its own CI job instead. `golangci-lint run` also runs the enabled formatters and reports their issues, so `fmt-check` duplicates that check - keep it anyway for the readable diff.\n\n## Lefthook Config\n\nLefthook is preferred over pre-commit for Go projects - it is a single Go binary, runs hooks in parallel, and needs no Python.\n\n```bash\ngo install github.com/evilmartians/lefthook/v2@v2.1.17   # needs Go 1.26+\nlefthook install\n```\n\n```yaml\n# lefthook.yml\nassert_lefthook_installed: true   # fail loudly instead of skipping every rule\n\npre-commit:\n  piped: true   # fail fast - stop at the first failing job\n  commands:\n    fmt:\n      glob: \"*.go\"\n      run: golangci-lint fmt {staged_files}\n      stage_fixed: true\n    lint:\n      glob: \"*.go\"\n      # Never pass a bare file list to `golangci-lint run`: a list spanning two\n      # directories is rejected outright, and one file of a multi-file package\n      # reports phantom `undefined:` typecheck errors. Lint the packages instead.\n      run: printf '%s\\n' {staged_files} | xargs -n1 dirname | sort -u | xargs golangci-lint run --fix\n      stage_fixed: true\n    mod-tidy:\n      glob: \"*.{go,mod,sum}\"\n      run: go mod tidy\n\npre-push:\n  commands:\n    test:\n      run: go test -race ./...\n```\n\n`piped: true` is fail-fast, not ordering - lefthook \"runs commands and scripts **sequentially** by default\", and `piped` adds \"Stop running commands and scripts if one of them fail.\" It cannot be combined with `parallel: true`.\n\n`jobs:` (added in lefthook 1.10.0) is the newer primitive alongside the `commands:`/`scripts:` split - \"Jobs provide a flexible way to define tasks, supporting both commands and scripts. Jobs can be grouped for advanced flow control.\" `commands:` is not deprecated and stays fully documented; reach for `jobs:` when you need grouping, nested control flow, or a mix of inline commands and scripts in one hook.\n\nFour more worth wiring:\n\n- `assert_lefthook_installed: true`, above, is the antidote to the dormancy footgun below: \"fail (with exit status 1) if `lefthook` executable can't be found in $PATH\".\n- `lefthook validate` in CI catches a malformed `lefthook.yml` before it silently disables hooks; `lefthook dump` prints the merged effective config when a hook does not behave as written.\n- A gitignored `lefthook-local.yml` lets a developer add or skip jobs without imposing it on teammates - \"This is useful when you want to use lefthook locally without imposing it on your teammates.\"\n- In a monorepo, give each job a `root:` pointing at its module directory; without it `go mod tidy` and `go tool` run against the repo root and fail.\n- `skip: [merge, rebase]` on the `mod-tidy` and lint jobs keeps them out of commits made while a merge or rebase is in progress, so resolving conflicts does not also restage a tidy rewrite or `--fix` edits.\n\nThe `pre-push` race suite repeats what CI already runs. Once it takes minutes, drop it or scope it to changed packages - otherwise developers learn to push with `LEFTHOOK=0`. Tests run from a hook also inherit git's hook environment (`GIT_DIR`, `GIT_INDEX_FILE`, `GIT_WORK_TREE`), so a test helper that runs `git init` or `git commit` in `t.TempDir()` acts on the real repository - clear those variables in `cmd.Env`.\n\nA conflict-free `git merge` never runs `pre-commit`: git invokes `pre-merge-commit` instead, and runs `pre-commit` only when you finish a conflicted merge with `git commit`. Mirror the checks under a `pre-merge-commit:` key, or merges land unlinted.\n\nHook commands resolve tools from the caller's PATH, so a missing binary fails the commit with a bare `exit 127`. Running them as `go tool <name>` (tool directive) makes the hook as reproducible as the build.\n\nBeta, but worth knowing: `ai:` declares LLM agent hooks in the same file - \"During `lefthook install`, lefthook generates the provider-specific settings file so that the agent calls `lefthook run <hook>` when the event fires\", for `claude`, `codex`, `cursor`, and `copilot`. See the [Lefthook Reference](references/lefthook-reference.md) for the wider config surface.\n\n## Project Structure\n\n```\nmyapp/\n  cmd/\n    myapp/\n      main.go              # Wire deps, call Run(), nothing else\n  internal/\n    user/                  # Domain logic, one package per domain\n      user.go\n      user_test.go\n      repository.go\n    transport/             # HTTP/gRPC handlers\n    storage/               # Database layer\n  migrations/\n    000001_create_users.up.sql\n    000001_create_users.down.sql\n  testdata/                # Test fixtures (ignored by go toolchain)\n  .golangci.yml\n  lefthook.yml\n  Justfile\n  go.mod\n  go.sum\n  Dockerfile\n```\n\n**Guidelines:**\n- `cmd/` - one directory per binary, keep `main.go` thin (~50 lines max)\n- `internal/` - all business logic goes here (compiler-enforced, cannot be imported externally)\n- `pkg/` - only add when another repo actually imports it today, not \"maybe someday\"\n- `testdata/` - test fixtures, golden files, fuzz corpus\n- `migrations/` - SQL migration files (timestamp or sequential versioned)\n\n## Daily Workflow\n\n```bash\njust fmt          # Format code\njust lint         # Run linter\njust test         # Run tests with race detection\njust check        # Full CI gate (fmt-check + lint + test)\njust test-watch   # Watch mode during development\njust generate     # Run go generate\njust tidy         # go mod tidy + verify\n```\n\n`go fix` is the toolchain-native complement to the `modernize` linter: Go 1.26 rebuilt it as a codebase modernizer - \"The venerable `go fix` command has been completely revamped and is now the home of Go's *modernizers*. It provides a dependable, push-button way to update Go code bases to the latest idioms and core library APIs.\" Run `go fix ./...` after a toolchain bump, before the linter has to complain. Go 1.27 added four more modernizers - \"The go fix command contains several new modernizers (atomictypes, embedlit, slicesbackward, and unsafefuncs)\" - and removed `fmtappendf`, so a 1.27 bump is a good moment to run it. It also renamed one: \"The existing `waitgroup` analyzer was renamed to `waitgroupgo`\", which matters if you disable it by name. For your own API migrations, annotate a deprecated function with a `//go:fix inline` directive and `go fix` (and golangci-lint's `govet` `inline` analyzer) rewrites its callers.\n\nOther Go 1.27 changes that touch this stack directly:\n\n- **Generic methods.** \"Go 1.27 now supports generic methods: a method declaration may declare its own type parameters.\"\n- **`encoding/json/v2`.** \"The encoding/json package is now backed by the v2 implementation\" - \"Marshaling and unmarshaling behavior is preserved, but the exact text of error messages may differ\", so tests that assert on JSON error strings break. The escape hatch is `GOEXPERIMENT=nojsonv2` at build time.\n- **`go test -json` gained an `OutputType` field**, annotating `\"Action\":\"output\"` lines. gotestsum v1.13.0 does not use it yet ([gotestsum#571](https://github.com/gotestyourself/gotestsum/issues/571)).\n- **`go mod tidy` reshapes `go.mod`** to \"at most two require blocks\". The first tidy after the bump rewrites the file, and the lefthook `mod-tidy` job stages that rewrite into whatever you commit next.\n- **Removed GODEBUG settings fail the build.** The `go` command now recognizes removed settings (`asynctimerchan`, `gotypesalias`, `tls10server`, `tlsrsakex`, `tls3des`, `tlsunsafeekm`, `x509keypairleaf`) in `go.mod` `godebug` lines and `//go:debug` comments - \"If they are set to an old value, the go command will fail.\" Delete them before bumping.\n- **gofmt alignment changed.** Go 1.27 warns that \"running gofmt from Go 1.27 on previously formatted code may produce minor whitespace changes\", and gofumpt v0.12.0 inherits it - expect a one-time formatting diff alongside the toolchain bump.\n\n## Footguns\n\nSeven failure modes that cost real debugging time, none of which produce an obvious error message.\n\n**Config placement is load-bearing.** `.golangci.yml` must sit at the repo root: golangci-lint searches the working dir and its parents, and editor Go plugins auto-detect only a root `.golangci.*`, so filing it under `.github/` costs in-IDE linting even if you pass `--config`. lefthook auto-discovers only the repo root or `.config/` - move `lefthook.yml` anywhere else and commits silently stop running hooks, because git invokes the hook directly and no task-runner recipe can intercept that.\n\n**lefthook is dormant until installed.** The binary being absent from PATH, or `lefthook install` never having run, both present as \"hooks just don't fire\" with no warning. Set `assert_lefthook_installed: true` so this fails loudly, pin lefthook as a repo tool, and make `lefthook install` part of onboarding. A leftover `core.hooksPath` (husky, pre-commit) is a third cause: `lefthook install` stops when it is set, until you run `lefthook install --reset-hooks-path`.\n\n**A stale lint cache invents issues.** golangci-lint can report failures in files that no longer exist on disk - typically after a branch switch or a deleted worktree. The costlier variant is nolintlint reporting a load-bearing `//nolint` directive as unused, which tempts you to delete a real suppression. Its main root cause was lost analyzer facts after an interrupted run, a changed linter set, or a partly cleaned cache ([#6807](https://github.com/golangci/golangci-lint/issues/6807)), fixed in v2.14.0 - so upgrade before debugging. The same bug can also *hide* real issues that CI then catches. Prove which side is lying with `GL_DEBUG=nolint_filter` before touching the code. If a phantom persists, move that suppression into `linters.exclusions.rules` rather than running `golangci-lint cache clean` before every gate, which turns a warm run of seconds into a cold one ten times longer. `cache clean` empties whatever `GOLANGCI_LINT_CACHE` resolves to in *that* shell, so running it outside a recipe that sets the variable cleans the wrong directory. When several worktrees share a checkout, give each its own cache with `GOLANGCI_LINT_CACHE=<worktree>/.golangci-cache` (\"the path must be absolute\") - and note the cache does not reliably invalidate on config, tool, or dependency changes, so fold those into the cache key if a phantom keeps returning.\n\n**Concurrent golangci-lint runs fail rather than queue.** The lock is a single file in the system temp dir, *not* per-`GOLANGCI_LINT_CACHE`, so per-worktree cache isolation does not prevent it. A second run waits five seconds, then exits with `parallel golangci-lint is running`. This bites hardest in a `just` recipe with `[parallel]` that runs `fmt` and `run` together, on green code. Set `run.allow-serial-runners: true` to wait indefinitely instead of failing, or `run.allow-parallel-runners: true` to drop the lock entirely.\n\n**Don't run two formatters against one gate.** Standalone `gofumpt -w` and `golangci-lint fmt` do not always agree on the same file, so a repo that fixes with one and gates with the other fails CI on code it just formatted. It recurs whenever gofumpt releases ahead of golangci-lint: v2.13.2 bundled gofumpt v0.11.0 against a standalone v0.12.0 (which changed how imports carrying comments and blank lines are laid out), and only v2.14.0 caught up. gofumpt master already carries a batch of unreleased output-changing fixes, so expect the next gap. Pick one as both fixer and gate - the Justfile and the hook above both use `golangci-lint fmt`.\n\n**A pinned linter older than your Go toolchain fails outright.** This is the same trap as the version floor above, and it usually surfaces first as a config-schema rejection: a config authored against a newer golangci-lint hits `additional properties ... not allowed` under the pinned CI version. Bump the CI pin and the local install together.\n\n**`govulncheck` fails on stdlib advisories, not just your code.** It scans against \"the Go version specified by the `go` command found on the PATH\". Locally, under `GOTOOLCHAIN=auto`, that is the newer of your installed `go` and the `toolchain` line in `go.mod`. In CI, setup-go exports `GOTOOLCHAIN=local`, so it is whatever `go-version` installed unless you use `go-version-file: go.mod`. Either way, a lagging toolchain red-lights CI on commits that touch zero Go code - and a failed test-and-lint job typically skips the release job downstream. When `govulncheck` reports vulnerabilities \"in the Go standard library\" all marked fixed in a patch you don't have, the fix is bumping the toolchain, not editing code.\n\n## CI/CD Pipeline (GitHub Actions)\n\n```yaml\nname: Go CI\non:\n  push:\n    branches: [main]\n  pull_request:\n\npermissions:\n  contents: read\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version-file: go.mod   # honours the `toolchain` line\n      - uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.14\n\n  test:\n    runs-on: ubuntu-latest\n    needs: lint\n    strategy:\n      matrix:\n        go-version: [stable, oldstable]\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version: ${{ matrix.go-version }}\n      - run: go install gotest.tools/gotestsum@v1.13.0\n      - name: Test\n        run: gotestsum --format github-actions --junitfile unit-tests.xml -- -race -coverprofile=coverage.out -covermode=atomic ./...\n      - uses: actions/upload-artifact@v7\n        if: always()\n        with:\n          name: test-results-${{ matrix.go-version }}\n          path: unit-tests.xml\n\n  security:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version-file: go.mod\n      - run: go install golang.org/x/vuln/cmd/govulncheck@v1.8.0\n      - run: govulncheck ./...\n```\n\nThree `setup-go` behaviours decide whether this workflow is fast or pathologically slow:\n\n- **It hashes a repo-root `go.mod`.** Caching is on by default, but a module in a subdirectory never matches, so every run logs a restore failure and cold-compiles the whole dependency tree. Point `cache-dependency-path` at the real file.\n- **It restores only the exact key.** There is no `restore-keys` prefix fallback, so every `go.sum` change is a fully cold run. If that hurts, set `cache: false` and use one `actions/cache` step with a `restore-keys` prefix, keyed per job so parallel jobs do not race on save.\n- **The cache is saved in a post step declared `post-if: success()`.** A job that fails saves nothing, so a cold run that times out stays cold forever and raising the timeout never breaks the loop. Split lint and test into separate jobs so one slow gate cannot starve the other's cache. The template's `run.timeout: 5m` is tight for a cold lint on a private repo's 2-vCPU `ubuntu-latest`.\n\nsetup-go also exports `GOTOOLCHAIN=local`, so the `go.mod` `toolchain` line is ignored unless you use `go-version-file`. The same rule breaks the `oldstable` matrix leg once you bump the `go` directive past it: the older `go` fails with `go.mod requires go >= ...` instead of downloading a newer toolchain. Drop `oldstable` at that point, or keep the directive one minor behind.\n\nThe action runs `golangci-lint config verify` itself before linting whenever a config file exists (input `verify`, default `true`), so a config the pinned binary rejects fails fast without an extra step. For incremental adoption with `only-new-issues: true`, grant `pull-requests: read`. Note that CI never runs your Justfile unless you install just (`extractions/setup-just`) - the template calls the tools directly. Action inputs for monorepos and `go.work` are in the [golangci-lint Reference](references/golangci-lint-reference.md).\n\n## Existing Project Migration\n\n```bash\n# 1. Install tools (golangci-lint as a binary - see Quick Start)\ncurl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0\ngo install mvdan.cc/gofumpt@v0.12.0\ngo install gotest.tools/gotestsum@v1.13.0\n\n# 2. Migrate existing golangci-lint v1 config\ngolangci-lint migrate\n\n# 3. Format codebase\ngofumpt -w .\n\n# 4. Run linter (fix what you can, nolint the rest)\ngolangci-lint run --fix ./...\n\n# 5. Replace go test with gotestsum in scripts/CI\n# Before: go test -v ./...\n# After:  gotestsum --format testname -- -race ./...\n\n# 6. Copy Justfile and lefthook.yml templates above\n# 7. Run: just check\n```\n\nFor incremental adoption on large codebases, use `only-new-issues: true` in the GitHub Action to only lint changed code. Outside the Action, `--new-from-merge-base=main` and `--new-from-rev=<rev>` do the same locally - see the [golangci-lint Reference](references/golangci-lint-reference.md) for the full set.\n\nExpect new findings after a toolchain bump: since Go 1.27, \"`go test` now invokes the `stdversion` vet check by default. This reports the use of standard library symbols that are too new for the Go version in force in the referring file\". Adjust the `go` directive or the call site rather than suppressing it. A linter bump does the same: on v2.14.0, `revive: enable-all-rules: true` switches on three new rules (`marshal-receiver`, `multiline-if-init`, `use-slices-concat`) and gosec re-enables G407, so budget for new findings on existing code. Go 1.27's vet adds a second `go test` failure: \"The printf analyzer now reports calls such as `fmt.Errorf(\"...: %w\", p)` in which the `%w` operand `p` has type `*E`, where the type `E` itself implements `error`\" - wrap the value, or make `*E` the error type.\n\n## Adjacent Tools\n\nNot part of the core stack, but the gaps most projects fill next:\n\n| Need | Tool | Why |\n|------|------|-----|\n| Structured logging | `log/slog` (stdlib) | The default since Go 1.21; the `sloglint` linter enforces a consistent call style |\n| Hot reload for a running service | [air](https://github.com/air-verse/air) or [wgo](https://github.com/bokwoon95/wgo) | `just test-watch` covers tests; neither `go run` nor gotestsum restarts a server on save |\n| Release binaries + changelog | [GoReleaser](https://goreleaser.com/) | Cross-compile, checksum, sign, and publish from one config |\n| Type-safe SQL from schema | [sqlc](https://sqlc.dev/) | Generates Go from the same SQL your migrations define, so `storage/` stays hand-written-free |\n\n## Reference Docs\n\n- [golangci-lint Reference](references/golangci-lint-reference.md) - v2 config, linter catalog, recommended sets, nolint syntax\n- [gofumpt Reference](references/gofumpt-reference.md) - formatting rules, editor integration, golangci-lint integration\n- [gotestsum Reference](references/gotestsum-reference.md) - output formats, watch mode, JUnit XML, CI recipes\n- [Go Testing Reference](references/go-testing-reference.md) - table-driven tests, mocking, benchmarks, coverage, fuzz testing\n- [golang-migrate Reference](references/go-migrate-reference.md) - CLI, library, embed.FS, transactions, pitfalls\n- [Justfile Reference](references/justfile-reference.md) - Go-specific recipes, task groups, lefthook integration\n- [Lefthook Reference](references/lefthook-reference.md) - job filtering, monorepo roots, remote configs, CLI, env vars\n\n## Resources\n\n- [Go Official Docs](https://go.dev/doc/)\n- [golangci-lint Docs](https://golangci-lint.run/)\n- [gofumpt](https://github.com/mvdan/gofumpt)\n- [gotestsum](https://github.com/gotestyourself/gotestsum)\n- [golang-migrate](https://github.com/golang-migrate/migrate)\n- [Lefthook](https://github.com/evilmartians/lefthook)\n- [just](https://github.com/casey/just)\n- [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck)\n- [Go 1.27 Release Notes](https://go.dev/doc/go1.27)\n- [Go Release History](https://go.dev/doc/devel/release)\n\nFile v0.6.0:_meta.json\n\n{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"go-dev\",\n  \"version\": \"0.6.0\",\n  \"publishedAt\": 1791288465096\n}\n\nFile v0.6.0:references/go-migrate-reference.md\n\n# golang-migrate Reference\n\nLatest: **v4.20.1** (2026-09-09). Built with Go 1.25/1.26. MIT license, 18K+ stars.\n\n**Pin v4.20.1, not v4.20.0.** A release-workflow bug meant v4.20.0 exists as a git tag but never reached Docker or the package registries - \"Due to a bug in the release workflow, GoReleaser failed and `v4.20.0` was not published to Docker or other package registries.\" v4.20.1 is that release redistributed, and carries no other changes.\n\nv4.20.0 is worth upgrading for regardless of the pin mechanics:\n\n- **S3 sources silently truncated at 1000 migrations** - \"fix(source/aws_s3): paginate ListObjects to load >1000 migrations\".\n- **Quadratic startup cost removed** - \"perf(source): build migrations index lazily to avoid quadratic startup\".\n- **Security, partial:** migrate's own code moved from `docker/docker` to the `moby/moby/api` and `moby/moby/client` modules, but v4.20.1's `go.mod` still lists `github.com/docker/docker v28.5.2+incompatible // indirect`, pulled in by `dhui/dktest`. Scanners keep flagging it - open issue [golang-migrate/migrate#1444](https://github.com/golang-migrate/migrate/issues/1444) \"Dependencies still trigger CVE-2026-41568\" (milestone v4.21.0).\n\n## Installation\n\n### CLI\n\n```bash\n# Homebrew (macOS)\nbrew install golang-migrate\n\n# Scoop (Windows)\nscoop install migrate\n\n# Pre-built binary\ncurl -L https://github.com/golang-migrate/migrate/releases/download/v4.20.1/migrate.linux-amd64.tar.gz | tar xvz\n\n# With Go (specify database driver via build tags)\ngo install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.20.1\n\n# Docker\ndocker run -v $(pwd)/migrations:/migrations --network host migrate/migrate \\\n    -path=/migrations/ -database \"postgres://localhost:5432/db\" up\n```\n\nMultiple drivers: `-tags 'postgres mysql sqlite3'`\n\n### Library\n\n```bash\ngo get github.com/golang-migrate/migrate/v4\n```\n\n## Migration File Naming\n\nFormat: `{version}_{title}.{direction}.sql`\n\n### Sequential (recommended for smaller teams)\n\n```bash\nmigrate create -ext sql -dir migrations -seq create_users_table\n```\n\nProduces:\n```\nmigrations/\n  000001_create_users_table.up.sql\n  000001_create_users_table.down.sql\n```\n\nControl zero-padding with `-digits N` (default: 6).\n\nTwo more `create` flags: `-format` takes \"a Go time format string\" for the version prefix, and `-tz` sets the timezone used to generate it.\n\n### Timestamp (better for larger teams)\n\n```bash\nmigrate create -ext sql -dir migrations create_users_table\n```\n\nProduces (default `-format` is `20060102150405`, a UTC timestamp):\n```\nmigrations/\n  20240405123456_create_users_table.up.sql\n  20240405123456_create_users_table.down.sql\n```\n\nEliminates version conflicts when multiple developers create migrations simultaneously.\n\nFor unix-epoch versions pass `-format unix` - \"If the string `\"unix\"` or `\"unixNano\"` is specified, then the seconds or nanoseconds since January 1, 1970 UTC respectively will be used.\" Combining `-seq` with any non-default `-format` fails with \"the seq and format options are mutually exclusive\".\n\n## CLI Commands\n\n```bash\n# Apply all pending migrations\nmigrate -path migrations -database \"$DATABASE_URL\" up\n\n# Apply next N migrations\nmigrate -path migrations -database \"$DATABASE_URL\" up 2\n\n# Revert last N migrations\nmigrate -path migrations -database \"$DATABASE_URL\" down 1\n\n# Revert ALL (interactive confirmation)\nmigrate -path migrations -database \"$DATABASE_URL\" down\n\n# Revert all without confirmation\nmigrate -path migrations -database \"$DATABASE_URL\" down -all\n\n# Check current version\nmigrate -path migrations -database \"$DATABASE_URL\" version\n\n# Migrate to specific version (up or down)\nmigrate -path migrations -database \"$DATABASE_URL\" goto 3\n\n# Fix dirty database state (set version without running migration)\nmigrate -path migrations -database \"$DATABASE_URL\" force 2\n\n# Drop everything (dangerous)\nmigrate -path migrations -database \"$DATABASE_URL\" drop -f\n\n# Create new migration files\nmigrate create -ext sql -dir migrations -seq add_email_column\n```\n\n**CLI options:**\n- `-source` - migration source (driver://url)\n- `-path` - shorthand for `-source=file://path`\n- `-database` - database connection URL\n- `-prefetch N` - migrations to load ahead (default 10)\n- `-lock-timeout N` - seconds to acquire lock (default 15)\n- `-verbose` - verbose logging\n\nHandles `SIGINT` gracefully, stopping at a safe point. Programmatically the same guarantee is exposed as a channel - \"To help prevent database corruptions, it supports graceful stops via `GracefulStop chan bool`\" - and the library takes your own logger via its `Logger` interface (\"Bring your own logger.\") rather than writing to stdout.\n\n## Up/Down Migration Patterns\n\n**Up migration** (`000001_create_users.up.sql`):\n```sql\nCREATE TABLE IF NOT EXISTS users (\n    id          SERIAL PRIMARY KEY,\n    email       VARCHAR(255) UNIQUE NOT NULL,\n    name        VARCHAR(100) NOT NULL,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX idx_users_email ON users (email);\n```\n\n**Down migration** (`000001_create_users.down.sql`):\n```sql\nDROP TABLE IF EXISTS users;\n```\n\n**Multi-statement with transaction** (`000002_add_status.up.sql`):\n```sql\nBEGIN;\n\nCREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned');\nALTER TABLE users ADD COLUMN status user_status NOT NULL DEFAULT 'active';\n\nCOMMIT;\n```\n\n**Down** (`000002_add_status.down.sql`):\n```sql\nBEGIN;\n\nALTER TABLE users DROP COLUMN status;\nDROP TYPE user_status;\n\nCOMMIT;\n```\n\n## Library Usage\n\n### Basic (URL-based)\n\n```go\nimport (\n    \"github.com/golang-migrate/migrate/v4\"\n    _ \"github.com/golang-migrate/migrate/v4/database/postgres\"\n    _ \"github.com/golang-migrate/migrate/v4/source/file\"\n)\n\nm, err := migrate.New(\n    \"file://migrations\",\n    \"postgres://user:pass@localhost:5432/mydb?sslmode=disable\")\nif err != nil {\n    log.Fatal(err)\n}\n\nif err := m.Up(); err != nil && err != migrate.ErrNoChange {\n    log.Fatal(err)\n}\n```\n\n### With Existing Connection\n\n```go\nimport (\n    \"database/sql\"\n    \"github.com/golang-migrate/migrate/v4\"\n    \"github.com/golang-migrate/migrate/v4/database/postgres\"\n    _ \"github.com/golang-migrate/migrate/v4/source/file\"\n)\n\ndb, _ := sql.Open(\"postgres\", connStr)\ndriver, _ := postgres.WithInstance(db, &postgres.Config{})\nm, _ := migrate.NewWithDatabaseInstance(\"file://migrations\", \"postgres\", driver)\n\nm.Up()        // Apply ALL pending migrations - not just the next one\nm.Steps(2)    // Apply exactly 2 up\nm.Steps(-1)   // Revert 1\nm.Version()   // Get current version + dirty flag\nm.Force(3)    // Set version without running\nm.Close()     // Close connections\n```\n\nAlways check for `migrate.ErrNoChange` when calling `Up()` - it means no new migrations exist and is not a real error.\n\n**`Up()` and `Steps(1)` are not interchangeable.** `Up()` applies every pending migration in one call; `Steps(1)` applies exactly one. Automation wired to `Up()` on merge will apply an entire backlog of DDL the first time it runs, which surprises teams who assumed each deploy advanced one version. Decide deliberately which semantics a given entry point has, and name it accordingly.\n\n## Embedding Migrations with embed.FS\n\nProduce self-contained binaries by embedding migrations at compile time:\n\n```go\npackage main\n\nimport (\n    \"embed\"\n    \"log\"\n\n    \"github.com/golang-migrate/migrate/v4\"\n    _ \"github.com/golang-migrate/migrate/v4/database/postgres\"\n    \"github.com/golang-migrate/migrate/v4/source/iofs\"\n)\n\n//go:embed migrations/*.sql\nvar migrationsFS embed.FS\n\nfunc runMigrations(databaseURL string) error {\n    d, err := iofs.New(migrationsFS, \"migrations\")\n    if err != nil {\n        return err\n    }\n\n    m, err := migrate.NewWithSourceInstance(\"iofs\", d, databaseURL)\n    if err != nil {\n        return err\n    }\n\n    if err := m.Up(); err != nil && err != migrate.ErrNoChange {\n        return err\n    }\n\n    return nil\n}\n```\n\nThe `//go:embed` directive path is relative to the Go source file containing it.\n\n## PostgreSQL-Specific\n\nURL format: `postgres://user:password@host:port/dbname?query`\n\n| Parameter | Description |\n|-----------|-------------|\n| `x-migrations-table` | Custom migrations table name (default: `schema_migrations`) |\n| `x-statement-timeout` | Abort statements exceeding N ms |\n| `x-multi-statement` | Enable multi-statement execution (default: false) |\n| `x-multi-statement-max-size` | Max statement size in bytes (default: 10MB) |\n| `search_path` | Schema search path |\n| `x-migrations-table-quoted` | Disable quoting of the migrations table name - \"By default, migrate quotes the migration table for SQL injection safety reasons. This option disable quoting\" |\n| `sslmode` | disable, require, verify-ca, verify-full |\n\nUses `pg_advisory_lock` for safe concurrent migrations.\n\n### pgx v5 driver\n\nFor projects already on `jackc/pgx/v5`, use the `pgx5` driver instead of `postgres` (which uses `lib/pq`). Same query parameters; different URL scheme, build tag, and import:\n\n- URL: `pgx5://user:password@host:port/dbname?query`\n- CLI: `go install -tags 'pgx5' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.20.1`\n- Library: `_ \"github.com/golang-migrate/migrate/v4/database/pgx/v5\"`\n\n## Transaction Handling\n\ngolang-migrate does **NOT** wrap a migration in a transaction at the library level, so the portable advice is to write explicit `BEGIN`/`COMMIT`.\n\nOne database-specific exception is worth knowing: \"In PostgreSQL running multiple SQL statements in one `Exec` executes them inside a transaction.\" A multi-statement Postgres migration is therefore already atomic in practice - but writing `BEGIN`/`COMMIT` anyway costs nothing, keeps the intent explicit, and stays correct on other engines:\n\n```sql\nBEGIN;\nALTER TABLE users ADD COLUMN phone VARCHAR(20);\nCREATE INDEX idx_users_phone ON users (phone);\nCOMMIT;\n```\n\n**Exception:** `CREATE INDEX CONCURRENTLY` cannot run inside a transaction. Put it in its own migration file without `BEGIN`/`COMMIT`:\n\n```sql\n-- 000003_add_concurrent_index.up.sql\nCREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_name ON users (name);\n```\n\n## Dirty Database State\n\nWhen a migration fails mid-execution, the database is marked \"dirty\" and no further migrations run.\n\nTo recover:\n1. Check current state: `migrate version` (shows version + dirty flag)\n2. Fix the issue manually in the database\n3. Force the correct version: `migrate force <version>`\n\nIf the failed migration partially applied, you may need to manually undo the partial changes before forcing.\n\n## Supported Databases\n\nPostgreSQL, PGX v4/v5, MySQL/MariaDB, SQLite, MS SQL Server, MongoDB, CockroachDB, YugabyteDB, ClickHouse, Cassandra/ScyllaDB, Neo4j, Cloud Spanner, Redshift, and more.\n\n## Migration Sources\n\nFilesystem, `embed.FS` (iofs), GitHub, GitLab, Bitbucket, AWS S3, Google Cloud Storage.\n\n## CI/CD Patterns\n\n### Test Migrations (up-down-up) in CI\n\n```yaml\nservices:\n  postgres:\n    image: postgres:17\n    env:\n      POSTGRES_USER: test\n      POSTGRES_PASSWORD: test\n      POSTGRES_DB: testdb\n    ports: [\"5432:5432\"]\nsteps:\n  - name: Run migrations up\n    run: migrate -path migrations -database \"postgresql://test:test@localhost:5432/testdb?sslmode=disable\" up\n  - name: Run migrations down\n    run: migrate -path migrations -database \"postgresql://test:test@localhost:5432/testdb?sslmode=disable\" down -all\n  - name: Run migrations up again\n    run: migrate -path migrations -database \"postgresql://test:test@localhost:5432/testdb?sslmode=disable\" up\n```\n\nThe up-down-up pattern validates both directions work correctly.\n\n## Common Pitfalls\n\n1. **Dirty state after failure** - most common issue. Must `force` correct version after manual fix\n2. **Version conflicts in teams** - use timestamp versioning for larger teams\n3. **Missing down migrations** - always write both directions, even if down is a no-op (add a SQL comment)\n4. **Non-transactional DDL** - `CREATE INDEX CONCURRENTLY` needs its own migration without `BEGIN`/`COMMIT`\n5. **URL encoding** - special characters in passwords must be percent-encoded\n6. **Empty files** - 0-byte migration files cause issues. Add a SQL comment if intentionally empty\n7. **Schema + role name clash** in PostgreSQL - `search_path` causes migrations table duplication. Fix: set `search_path=public` in URL\n8. **Never edit applied migrations** - treat merged migrations as immutable. Create new ones for corrections\n9. **Not every migration is reversible** - upstream's `MIGRATIONS.md` has a \"Reversibility of Migrations\" section; a destructive `up` (dropping a column, collapsing rows) cannot be undone by a `down` that only restores schema. Say so in a comment rather than shipping a `down` that silently loses data\n\n## Best Practices\n\n- Keep migrations small and focused - one logical change per migration\n- Always test up-down-up before merging\n- Use transactions for multi-statement migrations (when database supports it)\n- Embed migrations for production binaries (`embed.FS` + `iofs`)\n- Run migrations at app startup or as a separate step in the deploy pipeline\n- Use `migrate.ErrNoChange` check in programmatic usage\n- Pin `migrate` CLI version in CI for reproducibility\n\nFile v0.6.0:references/go-testing-reference.md\n\n# Go Testing Reference\n\nCovers Go testing best practices, patterns, and tooling as of Go 1.27 (August 2026).\n\n## Table-Driven Tests\n\nThe idiomatic Go testing pattern. Use named struct slices with `t.Run()` for subtests:\n\n```go\nfunc TestUserValidation(t *testing.T) {\n    tests := []struct {\n        name    string\n        user    User\n        wantErr error\n    }{\n        {\n            name:    \"valid user\",\n            user:    User{Email: \"[email protected]\", Age: 25},\n            wantErr: nil,\n        },\n        {\n            name:    \"missing email\",\n            user:    User{Age: 25},\n            wantErr: ErrInvalidEmail,\n        },\n        {\n            name:    \"negative age\",\n            user:    User{Email: \"[email protected]\", Age: -1},\n            wantErr: ErrInvalidAge,\n        },\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            err := tt.user.Validate()\n            if !errors.Is(err, tt.wantErr) {\n                t.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n            }\n        })\n    }\n}\n```\n\n**Note:** Since Go 1.22, the loop variable capture bug is fixed. `tt := tt` inside the loop is no longer needed, even with `t.Parallel()`.\n\n## Parallel Tests\n\n```go\nfunc TestParallel(t *testing.T) {\n    tests := []struct {\n        name  string\n        input int\n        want  int\n    }{\n        {\"double 1\", 1, 2},\n        {\"double 5\", 5, 10},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            t.Parallel() // Safe without tt := tt in Go 1.22+\n            got := Double(tt.input)\n            if got != tt.want {\n                t.Errorf(\"got %d, want %d\", got, tt.want)\n            }\n        })\n    }\n}\n```\n\nDefault parallelism = `GOMAXPROCS`. Override with `go test -parallel N`.\n\n## Testing Helpers (Go 1.14-1.27)\n\n### t.Helper()\n\nMark functions as test helpers so failures report the caller's line:\n\n```go\nfunc assertNoError(t testing.TB, err error) {\n    t.Helper()\n    if err != nil {\n        t.Fatalf(\"unexpected error: %v\", err)\n    }\n}\n```\n\nUse `testing.TB` as the parameter type so helpers work in both tests and benchmarks.\n\n### t.Cleanup(func()) - Go 1.14\n\nRegister cleanup that runs after the test and all subtests complete. LIFO order:\n\n```go\nfunc newTestDB(t *testing.T) *DB {\n    t.Helper()\n    db := openDB()\n    t.Cleanup(func() { db.Close() })\n    return db\n}\n```\n\n### t.TempDir() - Go 1.15\n\nAuto-cleaned temporary directory:\n\n```go\nfunc TestWriteFile(t *testing.T) {\n    dir := t.TempDir() // Removed automatically after test\n    path := filepath.Join(dir, \"output.txt\")\n    err := os.WriteFile(path, []byte(\"hello\"), 0o644)\n    require.NoError(t, err)\n}\n```\n\n### t.Setenv(key, value) - Go 1.17\n\nSet env var for test duration, restored on cleanup:\n\n```go\nfunc TestConfig(t *testing.T) {\n    t.Setenv(\"DATABASE_URL\", \"postgres://test@localhost/testdb\")\n    cfg := LoadConfig()\n    assert.Equal(t, \"postgres://test@localhost/testdb\", cfg.DatabaseURL)\n}\n```\n\nCannot be used with `t.Parallel()` (panics).\n\n### t.Chdir(dir) - Go 1.24\n\nChange the working directory for the duration of a test, restored on cleanup:\n\n```go\nfunc TestLoadFromCWD(t *testing.T) {\n    t.Chdir(\"testdata/project\")\n    cfg, err := LoadConfig()\n    require.NoError(t, err)\n}\n```\n\nLike `t.Setenv`, it cannot be combined with `t.Parallel()`.\n\n### t.Context() - Go 1.24\n\nReturns a context cancelled when the test finishes - \"The new `T.Context` and `B.Context` methods return a context that's canceled after the test completes and before test cleanup functions run.\":\n\n```go\nfunc TestWithContext(t *testing.T) {\n    ctx := t.Context()\n    result, err := service.Fetch(ctx, \"key\")\n    require.NoError(t, err)\n}\n```\n\n### t.ArtifactDir() - Go 1.26\n\nDirectory for test output files. It is **not** persisted by default: \"When the `-artifacts` flag is provided to `go test`, this directory will be located under the output directory (specified with `-outputdir`, or the current directory by default). Otherwise, artifacts are stored in a temporary directory which is removed after the test completes.\" With `-artifacts`, the first call logs the location as `=== ARTIFACTS TestName /path/to/artifact/dir`:\n\n```go\nfunc TestRender(t *testing.T) {\n    dir := t.ArtifactDir()\n    path := filepath.Join(dir, \"output.html\")\n    os.WriteFile(path, rendered, 0o644)\n}\n```\n\nAvailable as `T.ArtifactDir`, `B.ArtifactDir`, and `F.ArtifactDir`.\n\n### t.Attr(key, value) and t.Output() - Go 1.25\n\n`t.Attr` attaches a structured key/value attribute to a test, surfaced in `go test -json` output; `t.Output` returns an `io.Writer` whose writes are interleaved into that same stream rather than captured as raw log lines. gotestsum v1.13.0 added support for consuming these attributes.\n\n```go\nfunc TestPipeline(t *testing.T) {\n    t.Attr(\"dataset\", \"fixtures/large.json\")\n    fmt.Fprintln(t.Output(), \"stage 1 complete\")\n}\n```\n\n## Testify\n\nLatest: **v1.12.1** (August 2026). The most popular assertion library. Four packages:\n\nThere will be no v2 - \"Testify is being maintained at v1, no breaking changes will be accepted in this repo.\"\n\n### assert (soft assertions - test continues)\n\n```go\nimport \"github.com/stretchr/testify/assert\"\n\nfunc TestUser(t *testing.T) {\n    user := GetUser(\"123\")\n    assert.Equal(t, \"alice\", user.Name)\n    assert.NotEmpty(t, user.ID)\n    assert.NoError(t, user.Validate())\n    assert.Contains(t, user.Email, \"@\")\n    assert.Len(t, user.Roles, 2)\n    assert.True(t, user.Active)\n    assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Minute)\n}\n```\n\n### require (hard assertions - test stops on failure)\n\n```go\nimport \"github.com/stretchr/testify/require\"\n\nfunc TestFetch(t *testing.T) {\n    result, err := Fetch(\"key\")\n    require.NoError(t, err)    // Stops here if error\n    require.NotNil(t, result)  // Only runs if no error\n    assert.Equal(t, \"value\", result.Data)\n}\n```\n\n**Rule of thumb:** Use `require` for preconditions (error checks, nil checks), `assert` for the actual assertions.\n\n### suite (setup/teardown lifecycle)\n\n```go\nimport \"github.com/stretchr/testify/suite\"\n\ntype UserSuite struct {\n    suite.Suite\n    db *sql.DB\n}\n\nfunc (s *UserSuite) SetupSuite()    { s.db = connectTestDB() }\nfunc (s *UserSuite) TearDownSuite() { s.db.Close() }\nfunc (s *UserSuite) SetupTest()     { truncateAll(s.db) }\n\nfunc (s *UserSuite) TestCreate() {\n    err := CreateUser(s.db, \"alice\")\n    s.NoError(err)\n}\n\nfunc TestUserSuite(t *testing.T) { suite.Run(t, new(UserSuite)) }\n```\n\n### mock (expectations)\n\n```go\nimport \"github.com/stretchr/testify/mock\"\n\ntype MockRepo struct { mock.Mock }\n\nfunc (m *MockRepo) Get(id string) (*User, error) {\n    args := m.Called(id)\n    return args.Get(0).(*User), args.Error(1)\n}\n\nfunc TestService(t *testing.T) {\n    repo := new(MockRepo)\n    repo.On(\"Get\", \"123\").Return(&User{Name: \"alice\"}, nil)\n\n    svc := NewService(repo)\n    user, err := svc.FindUser(\"123\")\n    require.NoError(t, err)\n    assert.Equal(t, \"alice\", user.Name)\n\n    repo.AssertExpectations(t)\n}\n```\n\n## Mock Libraries\n\n| Library | Approach | Best For |\n|---------|----------|----------|\n| **go.uber.org/mock** (v0.6.0) | Code gen via `mockgen` | Precise expectations, call ordering |\n| **vektra/mockery** (v3.8.0) | Batch code gen, templates | Large codebases (5-30x faster than sequential mockgen) |\n| **matryer/moq** | Function-field based mocks | Lightweight, simple mocks |\n| **testify/mock** | Runtime (no codegen) | Quick mocking without generators |\n| **Hand-written** | Interface implementation | Full control, no dependencies |\n\n### gomock (go.uber.org/mock)\n\n```bash\ngo install go.uber.org/mock/mockgen@v0.6.0\n```\n\nmockgen has two supported modes: **source mode** (`-source=repository.go`, shown below) and **package mode** (`-destination=... <import path> <interfaces>`). Package mode replaced reflect mode - \"Deprecated reflect mode and replaced it with the new package mode.\" Do not write new `-reflect`-style invocations.\n\n```go\n//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=user\n\nfunc TestWithGomock(t *testing.T) {\n    ctrl := gomock.NewController(t)\n    repo := NewMockRepository(ctrl)\n    repo.EXPECT().Get(\"123\").Return(&User{Name: \"alice\"}, nil)\n\n    svc := NewService(repo)\n    user, _ := svc.FindUser(\"123\")\n    assert.Equal(t, \"alice\", user.Name)\n}\n```\n\n### mockery v3\n\nConfig-driven batch processing:\n\n```yaml\n# .mockery.yml (or .mockery.yaml - both are discovered)\npackages:\n  github.com/yourorg/myapp/internal/user:\n    interfaces:\n      Repository:\n      Service:\n```\n\n```bash\nmockery  # Generates all mocks in one pass\n```\n\n## Benchmarks\n\n### testing.B.Loop (Go 1.24+ - preferred)\n\n```go\nfunc BenchmarkSort(b *testing.B) {\n    data := generateData() // Setup excluded automatically\n    for b.Loop() {\n        sort.Ints(data)\n    }\n    // No b.ResetTimer() needed - setup/cleanup excluded automatically\n}\n```\n\nBenefits of `b.Loop()`:\n- Automatically excludes setup/cleanup from timing\n- Prevents dead-code elimination\n- Benchmark function called only once (faster)\n\nGo 1.26 removed the last reason to stay on `b.N` - \"The `B.Loop` method no longer prevents inlining in the loop body, which could lead to unanticipated allocation and slower benchmarks. With this fix, we expect that all benchmarks can be converted from the old `B.N` style to the new `B.Loop` style with no ill effects.\"\n\n### Old pattern (still works)\n\n```go\nfunc BenchmarkSortOld(b *testing.B) {\n    data := generateData()\n    b.ResetTimer()\n    for i := 0; i < b.N; i++ {\n        sort.Ints(data)\n    }\n}\n```\n\n### Sub-benchmarks\n\n```go\nfunc BenchmarkCache(b *testing.B) {\n    b.Run(\"Set\", func(b *testing.B) { for b.Loop() { cache.Set(\"k\", \"v\") } })\n    b.Run(\"Get\", func(b *testing.B) { for b.Loop() { cache.Get(\"k\") } })\n}\n```\n\n### Running and Comparing\n\n```bash\ngo test -bench=. -benchmem ./...\ngo test -bench=. -benchmem -count=5 ./... > old.txt\n# make changes\ngo test -bench=. -benchmem -count=5 ./... > new.txt\nbenchstat old.txt new.txt\n```\n\nAlways use `-benchmem` - allocations per op often matter more than raw speed.\n\n## Build Tags for Test Separation\n\nUse `//go:build` (not the old `// +build`):\n\n```go\n//go:build integration\n\npackage user_test\n\nfunc TestUserRepository_Integration(t *testing.T) {\n    db := connectRealDB(t)\n    // ...\n}\n```\n\nRun: `go test -tags=integration ./...`\n\n### Alternative: testing.Short()\n\n```go\nfunc TestSlow(t *testing.T) {\n    if testing.Short() {\n        t.Skip(\"skipping in short mode\")\n    }\n    // expensive test\n}\n```\n\nRun unit tests only: `go test -short ./...`\n\n### Alternative: Custom Flags\n\n```go\nvar integration = flag.Bool(\"integration\", false, \"run integration tests\")\n\nfunc TestMain(m *testing.M) {\n    flag.Parse()\n    os.Exit(m.Run())\n}\n\nfunc TestDB(t *testing.T) {\n    if !*integration {\n        t.Skip(\"pass -integration to run\")\n    }\n}\n```\n\nRun: `go test -integration ./...`\n\n## Coverage\n\n```bash\ngo test -cover ./...                           # Summary\ngo test -coverprofile=coverage.out ./...        # Generate profile\ngo tool cover -html=coverage.out                # HTML report\ngo tool cover -func=coverage.out                # Function-level summary\ngo test -coverpkg=./... ./...                   # Cross-package coverage\n```\n\n**Coverage modes:**\n- `-covermode=set` - did each statement run? (boolean)\n- `-covermode=count` - how many times?\n- `-covermode=atomic` - thread-safe count (use with `-race`)\n\n**Integration test coverage** (Go 1.20+):\n\n```bash\ngo build -cover -o myapp .\nGOCOVERDIR=./coverage_data ./myapp\ngo tool covdata textfmt -i=./coverage_data -o coverage.out\n```\n\n## Race Detector\n\n```bash\ngo test -race ./...\n```\n\n- Zero false positives - if it reports a race, it is real\n- ~2-10x slower, ~5-10x more memory\n- Only detects races on actually executed paths\n- **Always use `-race` in CI** - the single most important testing flag\n- Combine with `-count=N` for better detection\n- Set `-timeout` explicitly for heavy packages under `-race` - the default is \"10 minutes (10m)\", and a large package on a busy CI machine can hit it and panic\n\n### Goroutine Leak Profile (Go 1.27)\n\nGo 1.27 made the leak profile GA: \"A new profile type that reports leaked goroutines, previously available as an experiment in Go 1.26, is now generally available. The new profile type, named `goroutineleak`, is supported in the `runtime/pprof` package. It is also available as the `net/http/pprof` endpoint `/debug/pprof/goroutineleak`.\" A leaked goroutine is one \"blocked on some concurrency primitive ... that cannot possibly become unblocked\", detected via GC reachability, so leaks through globals or live locals can be missed. Drop any `GOEXPERIMENT=goroutineleakprofile` from build scripts - \"The `goroutineleakprofile` `GOEXPERIMENT` setting is now deleted.\"\n\n`WriteTo` runs the leak-detecting GC before writing; `Count` then reports that run's result:\n\n```go\nfunc TestNoLeaks(t *testing.T) {\n    runWorkload()\n    leaks := pprof.Lookup(\"goroutineleak\") // runtime/pprof\n    var buf bytes.Buffer\n    if err := leaks.WriteTo(&buf, 1); err != nil {\n        t.Fatal(err)\n    }\n    if n := leaks.Count(); n > 0 {\n        t.Errorf(\"%d leaked goroutines:\\n%s\", n, buf.String())\n    }\n}\n```\n\n## Fuzz Testing\n\n```go\nfunc FuzzParse(f *testing.F) {\n    // Seed corpus\n    f.Add(\"valid input\")\n    f.Add(\"\")\n    f.Add(\"edge case\")\n\n    f.Fuzz(func(t *testing.T, input string) {\n        result, err := Parse(input)\n        if err != nil {\n            return // Invalid input is fine\n        }\n        // Property: round-trip should preserve data\n        encoded := result.String()\n        result2, err := Parse(encoded)\n        if err != nil {\n            t.Errorf(\"round-trip failed: %v\", err)\n        }\n        if !reflect.DeepEqual(result, result2) {\n            t.Errorf(\"round-trip mismatch\")\n        }\n    })\n}\n```\n\nRun: `go test -fuzz=FuzzParse`\n\nSeed corpus stored in `testdata/fuzz/<FuzzTestName>/` - commit to version control.\n\nBest for: parsers, encoders/decoders, protocol implementations, user input handling.\n\n## Golden File Testing\n\nCompare output against a pre-approved reference file:\n\n```go\nvar update = flag.Bool(\"update\", false, \"update golden files\")\n\nfunc TestRender(t *testing.T) {\n    got := RenderTemplate(data)\n    golden := filepath.Join(\"testdata\", t.Name()+\".golden\")\n\n    if *update {\n        os.WriteFile(golden, []byte(got), 0o644)\n        return\n    }\n\n    want, err := os.ReadFile(golden)\n    require.NoError(t, err)\n    if diff := cmp.Diff(string(want), got); diff != \"\" {\n        t.Errorf(\"mismatch (-want +got):\\n%s\", diff)\n    }\n}\n```\n\nUpdate golden files: `go test -update ./...`\n\n**Keep goldens host-independent.** A golden whose content depends on `runtime.GOARCH`, `runtime.GOOS`, path separators, or the host's locale regenerates correctly on your machine and fails on the CI runner - the classic shape is a fixture written on arm64 macOS and asserted on amd64 Linux. Pin every host-derived input explicitly in the test rather than letting it default, or split the golden per platform.\n\nLibraries: `sebdah/goldie/v2`, `gotest.tools/v3/golden`\n\n## testdata/ Convention\n\nGo's toolchain ignores `testdata/` directories:\n\n```\ninternal/user/\n  user.go\n  user_test.go\n  testdata/\n    valid_user.json\n    invalid_user.json\n    fuzz/FuzzParse/   # Fuzz corpus\n```\n\nAccess fixtures with relative paths - `go test` sets CWD to the package directory:\n\n```go\ndata, err := os.ReadFile(\"testdata/valid_user.json\")\n```\n\n## Example Tests\n\nFunctions named `ExampleXxx()` serve as both tests and documentation:\n\n```go\nfunc ExampleReverse() {\n    fmt.Println(Reverse(\"hello\"))\n    // Output: olleh\n}\n```\n\nAppear in `go doc` output and run during `go test`.\n\n## HTTP Testing\n\n```go\nfunc TestHandler(t *testing.T) {\n    handler := NewHandler(mockService)\n\n    req := httptest.NewRequest(\"GET\", \"/users/123\", nil)\n    w := httptest.NewRecorder()\n\n    handler.ServeHTTP(w, req)\n\n    assert.Equal(t, http.StatusOK, w.Code)\n    assert.Contains(t, w.Body.String(), \"alice\")\n}\n\nfunc TestClient(t *testing.T) {\n    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        w.WriteHeader(http.StatusOK)\n        json.NewEncoder(w).Encode(User{Name: \"alice\"})\n    }))\n    t.Cleanup(srv.Close)\n\n    client := NewClient(srv.URL)\n    user, err := client.GetUser(\"123\")\n    require.NoError(t, err)\n    assert.Equal(t, \"alice\", user.Name)\n}\n```\n\n## testcontainers-go\n\nLatest: **v0.44.0**. Spin up ephemeral infrastructure for integration tests.\n\nUse the module-specific `Run` constructor, not `GenericContainer` - \"`GenericContainer` is the old way to create a container, and we recommend using `Run` instead, as it could be deprecated in the future.\" Note also that v0.43.0 changed `wait.ForSQL`: \"Users of `wait.ForSQL` need to follow the new API contract, using Moby's `network.Port` instead of `string`\".\n\n```go\nimport (\n    \"database/sql\"\n    \"testing\"\n\n    _ \"github.com/jackc/pgx/v5/stdlib\" // registers the \"pgx\" database/sql driver\n    \"github.com/stretchr/testify/require\"\n    \"github.com/testcontainers/testcontainers-go\"\n    \"github.com/testcontainers/testcontainers-go/modules/postgres\"\n)\n\nfunc TestUserRepo(t *testing.T) {\n    ctx := t.Context()\n\n    container, err := postgres.Run(ctx, \"postgres:17\",\n        postgres.WithDatabase(\"testdb\"), // must not be \"postgres\": Restore drops and recreates it\n        postgres.WithUsername(\"test\"),\n        postgres.WithPassword(\"test\"),\n        postgres.WithSQLDriver(\"pgx\"), // driver Snapshot/Restore pass to sql.Open\n        postgres.BasicWaitStrategies(),\n    )\n    testcontainers.CleanupContainer(t, container)\n    require.NoError(t, err)\n\n    connStr, err := container.ConnectionString(ctx, \"sslmode=disable\")\n    require.NoError(t, err)\n    require.NoError(t, runMigrations(connStr)) // e.g. golang-migrate, see go-migrate-reference.md\n    require.NoError(t, container.Snapshot(ctx))\n\n    t.Run(\"create\", func(t *testing.T) {\n        require.NoError(t, container.Restore(t.Context()))\n        db, err := sql.Open(\"pgx\", connStr) // open after Restore: it force-drops the database\n        require.NoError(t, err)\n        t.Cleanup(func() { db.Close() })\n        // Test against a freshly migrated database\n    })\n}\n```\n\nWhy each piece:\n\n- **`postgres.BasicWaitStrategies()`**, not a hand-rolled log wait. Upstream: it waits for \"`database system is ready to accept connections` twice, because it will restart itself after the first startup\", then for the port. A single-log or port-only wait races that restart and fails with `57P03 the database system is starting up`.\n- **`testcontainers.CleanupContainer(t, container)`** right after `Run`, before the error check - \"This should be the first call after [GenericContainer](...) or a module's Run(...) in a test before any error check. If container is nil, it's a no-op.\" Do not write `t.Cleanup(func() { container.Terminate(ctx) })` with `ctx := t.Context()`: that context \"is canceled just before Cleanup-registered functions are called\", so `Terminate` gets a dead context. `CleanupContainer` terminates with `context.Background()`.\n- **Migrate once, `Snapshot`, then `Restore` per test** instead of re-running migrations for every test, which can dominate suite time. `Snapshot(ctx, ...SnapshotOption)` copies the database into a template (default name `migrated_template`, override with `postgres.WithSnapshotName`); `Restore(ctx, ...SnapshotOption)` drops and recreates the database from it. Restore mutates the shared database, so tests using it cannot run in parallel. Both connect through `database/sql` with the driver name from `postgres.WithSQLDriver` (default `\"postgres\"`, i.e. `lib/pq`); if that driver is not registered they log and fall back to `docker exec psql`.\n- **The same trick works without a container for SQLite and other file-backed databases.** Run golang-migrate once per test binary (`sync.Once`, or `TestMain`) into a template file under `os.MkdirTemp`, then copy that file into each test's `t.TempDir()`. Copying a file is far cheaper than replaying every migration, which can otherwise be most of the suite's wall time. Keep the migration tests themselves on a fresh, unmigrated database, and expect an import cycle if a package's own in-package tests import the shared helper - give them a local copy.\n- **Ephemeral CI runners:** set `TESTCONTAINERS_RYUK_DISABLED=true`. Ryuk's reaper has nothing to collect on a runner that is thrown away, and many packages starting containers in parallel can time out waiting for it.\n\n## synctest (Go 1.25+)\n\nDeterministic testing of concurrent code. The stable API in Go 1.25+ is `synctest.Test(t, fn)`; the experimental `synctest.Run(fn)` from Go 1.24 (under `GOEXPERIMENT=synctest`) was renamed and now takes a `*testing.T`:\n\n```go\nimport \"testing/synctest\"\n\nfunc TestConcurrent(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ch := make(chan int)\n        go func() { ch <- 42 }()\n\n        synctest.Wait() // Waits for all goroutines to block\n        val := <-ch\n        assert.Equal(t, 42, val)\n    })\n}\n```\n\nGo 1.27 adds two conveniences: `synctest.Sleep`, which \"combines `time.Sleep` and `synctest.Wait`\", and `httptest.NewTestServer`, which \"creates a `Server` configured to use an in-memory fake network suitable for use with the `testing/synctest` package\" - the missing piece for testing HTTP clients under a fake clock.\n\n## Quick Reference: Test Flags\n\n```bash\ngo test ./...                          # Run all tests\ngo test -v ./...                       # Verbose\ngo test -race ./...                    # Race detection\ngo test -short ./...                   # Skip slow tests\ngo test -run TestFoo ./...             # Run matching tests\ngo test -run TestFoo/subcase ./...     # Run specific subtest\ngo test -count=1 ./...                 # Disable test cache\ngo test -tags=integration ./...        # Build tag\ngo test -parallel 4 ./...              # Max parallel tests\ngo test -timeout 10m ./...             # Test timeout\ngo test -bench=. -benchmem ./...       # Benchmarks\ngo test -fuzz=FuzzParse ./...          # Fuzzing\ngo test -coverprofile=c.out ./...      # Coverage\ngo test -covermode=atomic ./...        # Atomic coverage\ngo test -coverpkg=./... ./...          # Cross-package coverage\ngo test -artifacts ./...               # Keep t.ArtifactDir() output (Go 1.26+)\ngo test -artifacts -outputdir=./out ./...  # Keep it under ./out instead of CWD\n```\n\n**Go 1.27 annotates the JSON stream.** `go test -json` \"now annotates `\"Action\":\"output\"` lines with an optional new field `\"OutputType\"`\", distinguishing framework output from a test's own writes. Anything parsing `go test -json` - gotestsum, CI report generators, custom tooling - sees this field appear after a toolchain bump; consumers that validate the schema strictly may need updating.\n\n**A cached green run proves nothing.** `go test` caches results for unchanged packages and replays them, so a passing run may not have executed a single test. When a result matters - before a release, after a dependency bump, when confirming a fix - pass `-count=1` to force real execution. This is why the CI recipes in this skill use it.\n\nFile v0.6.0:references/gofumpt-reference.md\n\n# gofumpt Reference\n\nLatest: **v0.12.0** (2026-09-07). Based on Go 1.27's gofmt - \"This release is based on Go 1.27's gofmt, and requires Go 1.26 or later.\"\n\ngofumpt is a **strict superset of gofmt** - any code formatted by gofumpt produces zero changes when processed by gofmt. It adds 19 opinionated formatting rules on top, plus 3 opt-in extra rules.\n\n**Upgrading to v0.12.0 reformats imports.** Four fixes change output on real code, so expect a one-time diff: a std import carrying a comment is \"no longer moved into the top import group, as the comment stayed behind and ended up detached at the bottom of the group\"; moving a std import up \"no longer leaves an empty line where it used to be\"; a copyright header or package doc \"no longer makes gofumpt treat a single-line first declaration as multi-line\"; and an assignment whose right-hand side is split by a comment \"is now left alone\". Go 1.27's gofmt adds a fifth source of churn: a column-alignment fix means \"running gofmt from Go 1.27 on previously formatted code may produce minor whitespace changes\", and v0.12.0 inherits it from `go/printer`. Land the reformat as its own commit.\n\n**golangci-lint lags gofumpt.** golangci-lint v2.14.0 bundles `mvdan.cc/gofumpt v0.12.0`, so today `golangci-lint fmt` and a standalone v0.12.0 binary agree (v2.13.2 still vendored v0.11.0 and disagreed on exactly the cases above). The lag recurs whenever gofumpt releases first: master already carries 33 unreleased commits (2026-09-22/23), 16 of them output-changing `format:` fixes such as \"format: don't join imports whose comments would be left behind\". Do not use one as fixer and the other as gate.\n\n## Installation\n\n```bash\n# From source (recommended)\ngo install mvdan.cc/gofumpt@v0.12.0\n\n# Pre-built binaries from GitHub Releases\n# Available for darwin/linux/windows on amd64/arm64\n\n# Via gopls (no separate binary needed for editor use)\n# Configure your editor to tell gopls to use gofumpt formatting\n```\n\n## CLI Usage\n\n```bash\ngofumpt -w .                  # Format all Go files recursively, in-place\ngofumpt -l .                  # List files that differ from gofumpt style\ngofumpt -d main.go            # Show diff without modifying (non-zero exit if diff exists)\ngofumpt -w main.go            # Format single file in-place\ngofumpt -extra=group_params,clothe_returns .  # Explicit extra rules (the \"=\" is required)\ngofumpt -extra .              # All three extra rules, balance_calls included\ngofumpt -lang=go1.27 .        # Specify language version\ngofumpt -modpath=github.com/org/repo .  # Specify module path\ngofumpt -version              # Print version\ncat main.go | gofumpt         # Format from stdin\n```\n\n**Flags:**\n- `-w` - write result to file (instead of stdout)\n- `-l` - list files that differ\n- `-d` - display diff (non-zero exit if any diff, since v0.8.0)\n- `-extra` - enable extra rules. **Changed in v0.10.0:** \"The `-extra` flag now accepts a comma-separated list of rule names to enable individual extra rules, rather than enabling all of them at once.\" Bare `-extra` still enables all of them: `Extra` reports `IsBoolFlag() == true` (format/format.go), so the flag package calls `Set(\"true\")`, the same branch that sets `GroupParams`, `ClotheReturns` **and** `BalanceCalls`. Prefer the explicit `-extra=group_params,clothe_returns` to stay off the controversial `balance_calls`. Names are underscore-separated, comma-joined, and must follow `=` - as a bool flag, `-extra group_params` would treat `group_params` as a path. An unknown name fails with `unknown rule`\n- `-e` - report all errors (not just the first 10 on different lines)\n- `-lang` - language version (default: from go.mod)\n- `-modpath` - module path (affects import grouping)\n- `-s` - hidden, always enabled (simplification). Note \"the `-r` rewrite flag is removed in favor of `gofmt -r`, and the `-s` flag is hidden as it is always enabled\"\n\n**Skipped automatically:** `vendor/`, `testdata/`, generated files (unless given as explicit args). Obeys `ignore` directives in go.mod (Go 1.25+).\n\n## Default Rules (always applied)\n\nThese are the formatting rules gofumpt enforces beyond gofmt:\n\n1. **No empty lines around function bodies** - removes leading/trailing blank lines inside functions\n\n2. **No empty lines around a lone statement in a block** - `if err != nil {\\n\\n\\treturn err\\n}` removes the blank line\n\n3. **No empty lines before a simple error check** - no blank line between `foo, err := bar()` and `if err != nil {`\n\n4. **No empty lines following an assignment operator** - `foo :=\\n\"bar\"` becomes `foo := \"bar\"`\n\n5. **Composite literals use newlines consistently** - if any element is on a new line, braces go on their own lines\n\n6. **Empty field lists use a single line** - `struct {\\n}` becomes `struct{}`\n\n7. **std imports in a separate group at the top** - standard library imports grouped first, separated from third-party\n\n8. **Short case clauses on a single line** - `case 'a', 'b',\\n\\t'c':` becomes `case 'a', 'b', 'c':`\n\n9. **Multiline top-level declarations separated by empty lines** - two adjacent multi-line funcs get a blank line between them\n\n10. **Single var declarations not grouped** - `var (\\n\\tfoo = \"bar\"\\n)` becomes `var foo = \"bar\"`\n\n11. **Contiguous top-level declarations grouped together** - consecutive `var x = ...` grouped into `var (...)`\n\n12. **Simple var-declarations use short assignments** - `var s = \"str\"` becomes `s := \"str\"`\n\n13. **`-s` simplification always on** - `[][]int{[]int{1}}` becomes `[][]int{{1}}`\n\n14. **Octal literals use `0o` prefix** - `0755` becomes `0o755` (Go 1.13+ modules)\n\n15. **Non-directive comments start with whitespace** - `//Foo` becomes `// Foo` (but `//go:noinline` stays)\n\n16. **Composite literals: no leading/trailing empty lines**\n\n17. **Field lists: no leading/trailing empty lines**\n\n18. **Multi-line func params get `) {` on its own line** - trailing comma added for readability\n\n19. **Redundant parentheses dropped** (v0.10.0) - \"A new rule is introduced to drop unnecessary parentheses around expressions where the inner expression is unambiguous on its own, such as `f((3))`.\" Parentheses are kept where they carry meaning, such as on binary expressions, and around an expression starting with a composite literal like `(s{}.Foo())`, which needs them in an `if`/`for`/`switch` clause\n\n## Extra Rules (opt-in with `-extra`)\n\n1. **Group adjacent parameters with the same type** - `func Foo(bar string, baz string)` becomes `func Foo(bar, baz string)`\n\n2. **Clothe naked returns** (`clothe_returns`) - `return` in a function with named results becomes `return err` with explicit values (added in v0.9.0, moved to `-extra` in v0.9.2)\n\n3. **Balance multi-line calls** (`balance_calls`, v0.11.0) - matches the opening and closing parenthesis of a multi-line call in their use of newlines. Introduced as a default rule in v0.10.0 and walked back: \"The multi-line function call rule introduced in v0.10.0 proved controversial, so it is now the extra rule `balance_calls`, disabled by default.\" It only moves the closing parenthesis to its own line when the opening parenthesis ends a line\n\n## Editor Integration\n\n### VS Code\n\n```json\n{\n  \"go.useLanguageServer\": true,\n  \"gopls\": {\n    \"formatting.gofumpt\": true\n  }\n}\n```\n\n### GoLand\n\nFile Watchers: Settings > Tools > File Watchers > Add Custom Template\n- Program: path to `gofumpt`\n- Arguments: `-w $FilePath$`\n- Output: `$FilePath$`\n\n### Neovim (lspconfig)\n\n```lua\nrequire('lspconfig').gopls.setup({\n  settings = {\n    gopls = {\n      gofumpt = true\n    }\n  }\n})\n```\n\n### Vim (vim-go)\n\n```vim\nlet g:go_fmt_command=\"gopls\"\nlet g:go_gopls_gofumpt=1\n```\n\n### govim\n\n```vim\ncall govim#config#Set(\"Gofumpt\", 1)\n```\n\n### Helix\n\n```toml\n# ~/.config/helix/languages.toml\n[language-server.gopls.config]\n\"formatting.gofumpt\" = true\n```\n\n### Zed\n\n```json\n{\n  \"lsp\": {\n    \"gopls\": {\n      \"initialization_options\": {\n        \"gofumpt\": true\n      }\n    }\n  }\n}\n```\n\n### Emacs (lsp-mode 8.0.0+)\n\n```elisp\n(setq lsp-go-use-gofumpt t)\n```\n\n### Emacs (eglot)\n\n```elisp\n(setq-default eglot-workspace-configuration\n  '((:gopls . ((gofumpt . t)))))\n```\n\n### Sublime Text (ST4 with LSP)\n\n```json\n{\n  \"lsp_format_on_save\": true,\n  \"clients\": {\n    \"gopls\": {\n      \"enabled\": true,\n      \"initializationOptions\": {\n        \"gofumpt\": true\n      }\n    }\n  }\n}\n```\n\n## golangci-lint v2 Integration\n\nIn golangci-lint v2, gofumpt is a **formatter** (not a linter):\n\n```yaml\n# .golangci.yml\nformatters:\n  enable:\n    - gofumpt\n  settings:\n    gofumpt:\n      module-path: github.com/org/project\n      extra:\n        group-params: true\n        clothe-returns: true\n        balance-calls: false\n```\n\nRun: `golangci-lint fmt`\n\nSince golangci-lint v2.13.0 (which bundles gofumpt 0.11.0) the extra rules are selected individually - \"`gofumpt`: from 0.9.2 to 0.11.0 (new options: `extra.group-params`, `extra.clothe-returns`, `extra.balance-calls`)\".\n\n**`extra-rules: true` is deprecated, and it is not a neutral shorthand.** golangci-lint marks it `# Deprecated: use `extra` instead.` and warns on every run: `` `extra-rules` is deprecated, please use `extra.group-params` instead ``. More importantly it enables *all three* rules, `balance_calls` included - in gofumpt's own code `ExtraRules` calls `Extra.Set(\"true\")`, whose branch sets `GroupParams`, `ClotheReturns` **and** `BalanceCalls`. Since `balance_calls` is the rule gofumpt deliberately demoted as controversial and disabled by default, `extra-rules: true` silently opts you back into it. Use the `extra:` map. The next golangci-lint release rewrites that warning to \"please use `extra.group-params` and `extra.clothe-returns` instead\" (golangci-lint#6835) - following it drops `balance_calls`, which is what the `extra:` map above already does.\n\n## Diagnostics\n\nInsert `//gofumpt:diagnose` in any Go file and run gofumpt - it rewrites the comment with version and config info:\n\n```go\n//gofumpt:diagnose version: v0.12.0 flags: -lang=go1.27 -modpath=github.com/org/project\n```\n\n## Go API\n\n```go\nimport \"mvdan.cc/gofumpt/format\"\n\nformatted, err := format.Source(src, format.Options{\n    LangVersion: \"go1.26\",\n    ModulePath:  \"github.com/org/project\",\n    Extra: format.Extra{\n        GroupParams:   true,\n        ClotheReturns: true,\n        BalanceCalls:  false,\n    },\n})\n```\n\n`Options.ExtraRules` is deprecated in favour of `Options.Extra`. To stay source-compatible across releases that add new extra rules, set them by name instead of by field - \"Go API users who wish to avoid build errors in such cases can use the string API in [Extra.Set]\".\n\n## Recent Changes\n\n| Version | Date | Key Changes |\n|---------|------|-------------|\n| v0.12.0 | Sep 2026 | Based on Go 1.27's gofmt; **requires Go 1.26+**. Four import/blank-line fixes: std imports with comments stay put, no orphan empty line when a std import moves up, copyright headers no longer force a blank line after the first declaration, comment-split assignments left alone |\n| v0.11.0 | Jul 2026 | Multi-line call rule demoted to the `balance_calls` extra rule (disabled by default); stable single-pass output for a lone var next to a single-element var group |\n| v0.10.0 | May 2026 | Based on Go 1.26's gofmt; requires Go 1.25+. **Breaking:** `-extra` takes a comma-separated rule list instead of a boolean. New default rule dropping redundant parentheses |\n| v0.9.2 | Oct 2025 | \"Clothe naked returns\" moved to `-extra` flag |\n| v0.9.1 | Sep 2025 | Bugfix: comment directive detection |\n| v0.9.0 | Sep 2025 | Based on Go 1.25's gofmt. New \"clothe naked returns\" rule. Obeys go.mod `ignore`. Speed-up via x/mod/modfile |\n| v0.8.0 | Apr 2025 | Based on Go 1.24's gofmt. `-d` returns non-zero on diff |\n\nFile v0.6.0:references/golangci-lint-reference.md\n\n# golangci-lint v2 Reference\n\nLatest: **v2.14.0** (2026-09-24). Requires `version: \"2\"` in config.\n\n**Go version floor:** \"golangci-lint supports Go versions lower or equal to the Go version used to compile it.\" Go 1.27 support arrived in v2.13.0 (\"🎉 go1.27 support\"), so a Go 1.27 project needs v2.13 or newer - an older pin fails outright rather than degrading. `go install` of v2.14.0 itself requires Go 1.26 (its `go.mod` says `go 1.26.0`).\n\n## Installation\n\n```bash\n# Binary (recommended)\ncurl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0\n\n# Homebrew - \"Homebrew can use an unexpected version of Go to build the binary\",\n# and it cannot pin a version. Prefer the binary installer.\nbrew install golangci-lint\n\n# Docker\ndocker run --rm -v $(pwd):/app -w /app golangci/golangci-lint:v2.14.0 golangci-lint run\n\n# mise (uses the aqua backend, so it fetches the GitHub binary)\nmise use -g golangci-lint@2.14.0\n\n# go install (not recommended - dependency conflicts possible)\ngo install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0\n```\n\n**Upstream recommends binary installation and warns against the tools pattern:** \"Using `go install`/`go get`, \\\"tools pattern\\\", and `tool` command/directives installations aren't guaranteed to work. We recommend using binary installation.\" Seven reasons are listed, the load-bearing one for shared repos being that \"the dependencies of a tool can modify the dependencies of another tool or your project\". There is a blunt \"We don't recommend using `go tool`\" on top.\n\nIf you need it in `go.mod` anyway, isolate it behind a dedicated module file so it cannot perturb your project's graph - \"the best approach is to use a dedicated module or module file to isolate golangci-lint from other tools or dependencies\":\n\n```bash\ngo mod init -modfile=golangci-lint.mod github.com/org/repo/golangci-lint\ngo get -tool -modfile=golangci-lint.mod github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.14.0   # rerun with a new tag to update\ngo tool -modfile=golangci-lint.mod golangci-lint run\n```\n\n## Commands\n\n```bash\ngolangci-lint run              # Lint (default: ./...)\ngolangci-lint run --fix        # Lint and apply autofixes\ngolangci-lint fmt              # Format code (v2 feature)\ngolangci-lint fmt --diff       # Show formatting diff\ngolangci-lint migrate          # Migrate v1 config to v2\ngolangci-lint linters          # List enabled linters\ngolangci-lint help linters     # List all available linters\ngolangci-lint formatters       # List enabled formatters\ngolangci-lint config path      # Show which config file is used\ngolangci-lint cache clean      # Clear analysis cache (fixes phantom issues from stale results)\ngolangci-lint cache status     # Show cache directory and size\ngolangci-lint custom           # Build a binary with module plugins (.custom-gcl.yml)\ngolangci-lint version          # Print version\ngolangci-lint run --fast-only  # Run fast linters only (for editors)\ngolangci-lint run --default=none --enable=govet  # Run specific linters\n```\n\n`run` also runs the enabled formatters and reports their issues, \"but it does not format the code\"; \"To apply both linter fixes and formatting, use `golangci-lint run --fix`\" (docs/content/docs/configuration/cli.md). So a separate `golangci-lint fmt --diff` gate duplicates the formatter check that `run` already fails on - harmless, and its diff is easier to read than a lint issue.\n\n## Config File Structure\n\nConfig file: `.golangci.yml` (searched in CWD, then parent dirs, then home).\n\nJSON Schema: use the **versioned** URL matching your binary, e.g. `https://golangci-lint.run/jsonschema/golangci.v2.14.jsonschema.json`. The unversioned `golangci.jsonschema.json` follows the *latest release* - the release bot rewrites it after each tag (\"docs: update documentation assets\") - so it drifts ahead of an older pinned binary. Master is `golangci.next.jsonschema.json`, the file embedded in the binary for `config verify` (`jsonschema/jsonschema.go:12`). `golangci-lint config verify` against the installed binary is the authoritative check.\n\n`.golangci.reference.yml` in the repo lists every supported option with descriptions and defaults - \"There is a `.golangci.reference.yml` file with all supported options, their descriptions, and default values.\"\n\n**Cache isolation:** golangci-lint honours `GOLANGCI_LINT_CACHE`. Give each git worktree its own value so a deleted branch's cached results cannot resurface as issues in files that no longer exist. The cache does not reliably invalidate on config, tool-version, or dependency changes, so if a phantom issue keeps returning, fold those inputs into the cache path rather than clearing by hand each time.\n\n`GOLANGCI_LINT_CACHE` overrides the default `golangci-lint` dir under the user cache dir, and \"the path must be absolute\" (docs/content/docs/configuration/cli.md). `golangci-lint cache clean` deletes whatever `GOLANGCI_LINT_CACHE` resolves to in the shell that runs it (`cache.DefaultDir()`), so running it by hand outside a recipe that sets the var wipes the default dir and leaves the recipe's cache untouched. Clean from inside the same environment the lint runs in.\n\n**Exit 7 after \"0 issues.\" is a failure, not a pass.** Exit codes are `1` issues found, `3` failure, `4` timeout, `7` an error was logged (`pkg/exitcodes/exitcodes.go`) - e.g. a typecheck error the linter \"couldn't parse and ... just logged\" (`pkg/commands/run.go:479`). The common trigger is a cache that cannot be written - a sandboxed agent, read-only `HOME`, a locked-down CI runner - which logs `operation not permitted` under `go-build` and also drops analyzer facts on every run, the same root cause as the phantom-nolintlint footgun. Point both `GOCACHE` and `GOLANGCI_LINT_CACHE` at writable absolute paths, and read stderr before trusting the summary line.\n\n**Cache isolation does not buy you concurrency.** The run lock is a single file in the system temp dir - `filepath.Join(os.TempDir(), \"golangci-lint.lock\")` - so two runs collide no matter how their caches are separated. A second run retries for five seconds and then exits with `parallel golangci-lint is running`. Two knobs change this:\n\n```yaml\nrun:\n  allow-parallel-runners: true   # \"Allow multiple parallel golangci-lint instances running.\" Drops the lock.\n  allow-serial-runners: true     # \"Allow multiple golangci-lint instances running, but serialize them around a lock.\" Waits instead of failing.\n```\n\nReach for one of them before putting `golangci-lint fmt` and `golangci-lint run` in the same `just` recipe under `[parallel]`, or in concurrent CI steps - otherwise the failure lands on green code and looks like a lint bug.\n\n**Debugging the nolint filter.** When nolintlint claims a live `//nolint` directive is unused, `GL_DEBUG=nolint_filter` prints what the filter actually received - the fastest way to tell a stale cache from a genuinely dead suppression before you delete a real one. Other useful keys: `GL_DEBUG=exec` (the lock file), `pkgcache`, `linters_context`, `enabled_linters`.\n\n```yaml\nversion: \"2\"  # REQUIRED\n\nrun:\n  timeout: 5m             # Default: 0 (disabled in v2)\n  tests: true             # Include test files\n  build-tags: []\n  go: \"\"                  # Default: from go.mod\n  concurrency: 0          # 0 = auto (CPU count)\n  relative-path-mode: cfg # cfg | gomod | gitroot | wd\n  issues-exit-code: 1     # Exit code when issues were found\n  modules-download-mode: readonly  # mod | readonly | vendor\n  allow-parallel-runners: false    # Drop the global run lock\n  allow-serial-runners: false      # Queue on the lock instead of failing\n  enable-build-vcs: false          # Default false, which implies `-buildvcs=false`\n\nlinters:\n  default: standard       # standard | all | none | fast\n  enable: [...]\n  disable: [...]\n  settings:\n    # Per-linter config (was top-level linters-settings in v1)\n    govet:\n      enable: [shadow]\n    revive:\n      enable-all-rules: true\n  exclusions:\n    generated: strict      # strict | lax | disable\n    warn-unused: true\n    presets:               # NOT enabled by default in v2\n      - comments\n      - std-error-handling\n      - common-false-positives\n    rules:\n      - path: _test\\.go\n        linters: [errcheck, dupl, gosec]\n    paths:\n      - third_party$\n      - vendor$\n\nformatters:\n  enable: [gofumpt, goimports]\n  settings:\n    gofumpt:\n      extra:                 # not `extra-rules: true` - deprecated, and it also enables balance_calls\n        group-params: true\n        clothe-returns: true\n        balance-calls: false\n    gci:\n      sections: [standard, default, \"prefix(github.com/myorg/myrepo)\"]\n  exclusions:\n    generated: strict\n\nissues:\n  max-issues-per-linter: 50   # 0 = unlimited\n  max-same-issues: 3          # 0 = unlimited\n  new: false\n  new-from-merge-base: \"\"     # e.g., \"main\"\n  fix: false\n\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      colors: true\n  sort-order: [linter, file]\n  show-stats: true\n  path-mode: \"\"           # \"abs\" shows absolute paths instead of relative ones\n  path-prefix: \"\"         # Prepended to every reported path\n\nseverity:\n  default: \"\"\n  rules:\n    - linters: [dupl]\n      severity: info\n```\n\n## Default Linters (the \"standard\" set)\n\nEnabled when `default: standard` (the default):\n\n1. **errcheck** - unchecked errors\n2. **govet** - suspicious constructs (like `go vet`)\n3. **ineffassign** - unused assignments\n4. **staticcheck** - comprehensive static analysis (includes gosimple + stylecheck in v2)\n5. **unused** - unused code\n\n## Linter Catalog by Category\n\n### Bug Detection\n\n| Linter | Description | Autofix |\n|--------|-------------|---------|\n| asasalint | `[]any` passed as a single `any` to a `...any` func | |\n| bidichk | \"Checks for dangerous unicode character sequences\" (Trojan Source) | |\n| bodyclose | HTTP response body not closed | |\n| contextcheck | Non-inherited context usage | |\n| durationcheck | Two durations multiplied together | |\n| errcheck | Unchecked errors (default) | |\n| errchkjson | Types passed to json encoding | |\n| errorlint | Go 1.13+ error wrapping issues | Yes |\n| exhaustive | Enum switch exhaustiveness | |\n| fatcontext | Nested contexts in loops | Yes |\n| gosec | Security problems | |\n| govet | Suspicious constructs (default) | Yes |\n| makezero | Slices with non-zero initial length | |\n| musttag | Field tags in marshaled structs | |\n| loggercheck | Odd key-value pairs in slog/zap/logr/klog calls | |\n| nilerr | Returns nil when err is not nil | |\n| nilnesserr | err != nil but returns different nil error | |\n| noctx | Missing context.Context usage | |\n| nosprintfhostport | `Sprintf` building `host:port` (breaks IPv6; use `net.JoinHostPort`) | |\n| reassign | Package variables reassigned (e.g. `io.EOF = ...`) | |\n| rowserrcheck | Rows.Err not checked | |\n| sqlclosecheck | sql.Rows/Stmt not closed | |\n| staticcheck | Comprehensive static analysis (default) | Yes |\n| testifylint | Testify usage issues | Yes |\n\n### Performance\n\n| Linter | Description | Autofix |\n|--------|-------------|---------|\n| fatcontext | Context allocation in loops | Yes |\n| perfsprint | Faster alternatives to fmt.Sprintf | Yes |\n| prealloc | Slice pre-allocation opportunities | |\n\n### Style & Code Quality\n\n| Linter | Description | Autofix |\n|--------|-------------|---------|\n| copyloopvar | Loop variable copies | Yes |\n| dupl | Duplicate code fragments | |\n| dupword | Duplicate words in source | Yes |\n| err113 | Error handling expressions | Yes |\n| errname | Sentinel error naming conventions | |\n| exptostd | Replace x/exp with stdlib | Yes |\n| goconst | Repeated strings that could be constants | |\n| gocritic | Bugs, performance, style diagnostics | Yes |\n| godot | Comments ending in period | Yes |\n| intrange | Integer range in for loops | Yes |\n| mirror | bytes/strings mirror patterns | Yes |\n| misspell | Misspelled English words | Yes |\n| modernize | Suggests modern Go language features | |\n| nakedret | Naked returns | Yes |\n| nestif | Deeply nested if statements | |\n| nolintlint | Ill-formed nolint directives | Yes |\n| nonamedreturns | Named returns | |\n| predeclared | Shadowing predeclared identifiers | |\n| revive | Fast, configurable meta-linter | |\n| sloglint | log/slog code style | Yes |\n| thelper | Missing t.Helper() in test helpers | |\n| unconvert | Unnecessary type conversions | |\n| unparam | Unused function parameters | |\n| usestdlibvars | Use stdlib variables/constants | Yes |\n| usetesting | Use testing package replacements | Yes |\n| wastedassign | Wasted assignments | |\n| whitespace | Unnecessary newlines | Yes |\n| wrapcheck | Error wrapping from external packages | |\n| wsl_v5 | Whitespace/cuddling style (replaces `wsl`) | |\n\n### Also Available (not in the sets above)\n\n| Linter | Description | Autofix |\n|--------|-------------|---------|\n| arangolint | ArangoDB query issues, incl. injection | |\n| canonicalheader | Non-canonical HTTP header keys | Yes |\n| clickhouselint | ClickHouse driver misuse (v2.12.0+) | |\n| containedctx | \"detects struct contained context.Context field\" | |\n| cyclop | \"Checks function and package cyclomatic complexity\" | |\n| depguard | \"checks if package imports are in a list of acceptable packages\" - allow/deny rules that enforce package and architecture boundaries | |\n| embeddedstructfieldcheck | Embedded-field placement in structs | |\n| forbidigo | \"Forbids identifiers\" by regexp; default forbids `fmt.Print*`, `print`, `println` | |\n| forcetypeassert | \"Find forced type assertions\" - `x.(T)` without the `, ok` form | |\n| funcorder | Constructor/method ordering within a file | |\n| gocheckcompilerdirectives | \"Checks that go compiler directive comments (//go:) are valid\" | |\n| gochecksumtype | Exhaustiveness for sum types | |\n| gocognit | \"Computes and checks the cognitive complexity of functions\" | |\n| godoclint | Godoc comment conventions | |\n| gomoddirectives | Validates go.mod directives: `toolchain-pattern`, `tool-forbidden`, `go-version-pattern`, `replace-*` - the enforcement side of pinning the toolchain | |\n| gomodguard_v2 | Allow/blocklist direct module dependencies | |\n| iface | Interface misuse, incl. unused methods | |\n| importas | \"Enforces consistent import aliases\" | Yes |\n| iotamixing | Mixed iota and explicit values in a const block | |\n| nilnil | Returning both a nil value and a nil error | |\n| noinlineerr | Inline `if err := f(); err != nil` declarations | |\n| paralleltest | \"Detects missing usage of t.Parallel() method in your Go test\" | |\n| protogetter | Direct proto field access instead of getters | Yes |\n| recvcheck | Mixed pointer/value receivers on one type | |\n| spancheck | OpenTelemetry/Census span mistakes | |\n| tagalign | Struct tag alignment | Yes |\n| testableexamples | Examples without an `// Output:` comment (compiled, never run) | |\n| testpackage | Requires the external `_test` package | |\n| tparallel | \"detects inappropriate usage of t.Parallel() method in your Go test codes\" | |\n| unqueryvet | `SELECT *`, N+1 queries, SQL injection, tx leaks | |\n\n### Deprecated Names\n\n| Deprecated | Replacement | Since |\n|-----------|-------------|-------|\n| `wsl` | `wsl_v5` | v2.2.0 |\n| `gomodguard` | `gomodguard_v2` | v2.12.0 |\n| `exhaustruct` | `exhaustruct_v5` | v2.13.0 |\n\nDeprecated names still resolve but will be removed; `golangci-lint help linters` marks them `[deprecated]`.\n\n## Recommended Linter Sets\n\n### Minimal (large existing codebases)\n\n```yaml\nlinters:\n  default: standard\n  enable:\n    - bodyclose\n    - errorlint\n    - gosec\n    - noctx\n    - sqlclosecheck\n```\n\n### Comprehensive (new projects - recommended)\n\n```yaml\nlinters:\n  default: standard\n  enable:\n    - bodyclose\n    - copyloopvar\n    - dupl\n    - durationcheck\n    - err113\n    - errname\n    - errorlint\n    - exhaustive\n    - exptostd\n    - fatcontext\n    - goconst\n    - gocritic\n    - gosec\n    - intrange\n    - misspell\n    - modernize\n    - musttag\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - nonamedreturns\n    - perfsprint\n    - prealloc\n    - revive\n    - sqlclosecheck\n    - testifylint\n    - thelper\n    - unconvert\n    - unparam\n    - usestdlibvars\n    - usetesting\n    - wastedassign\n    - whitespace\n    - wrapcheck\n```\n\n### Maximum (enable all, disable noisy ones)\n\nPin the golangci-lint version exactly when using `all` - upstream: \"It's important to have reproducible CI: don't start to fail all builds at the same time. With golangci-lint this can happen if you use option `linters.default: all` and a new linter is added\" (docs/content/docs/welcome/install/ci.md). `version: latest` in the Action or a `brew upgrade` pulls in new linters unannounced; a minor pin like `v2.14` still floats across patch releases, which bump linter versions (\"or even without `linters.default: all` when one upstream linter is upgraded\").\n\n```yaml\nlinters:\n  default: all\n  disable:\n    - exhaustruct_v5   # Too strict for most projects (v2.13.0+ name; was `exhaustruct`)\n    - gochecknoglobals # Impractical for many codebases\n    - gochecknoinits   # Too restrictive\n    - ireturn          # Controversial\n    - varnamelen       # Too opinionated\n    - mnd              # Very noisy (magic numbers)\n    - lll              # Line length is editor config territory\n    - funlen           # Arbitrary length limits\n    - godox            # FIXME/TODO are normal in active dev\n    - wsl_v5           # Very opinionated whitespace rules\n```\n\n## Nolint Directive Syntax\n\n```go\n// Suppress specific linters on this line:\nvar bad int //nolint:revive,unused\n\n// Suppress all linters:\nvar bad int //nolint:all\n\n// Suppress for a function/block:\n//nolint:gocyclo\nfunc complexFunction() { ... }\n\n// Suppress for entire file (before package):\n//nolint:unparam\npackage pkg\n\n// With justification (recommended, enforced by nolintlint):\nvar x int //nolint:revive // legacy code, scheduled for cleanup\n```\n\n**Syntax rules** - nolint is a Go directive, not a comment:\n- NO space between `//` and `nolint`\n- NO space between `nolint` and `:`\n- NO space between `:` and linter names\n\nValid: `//nolint:xxx` | Invalid: `// nolint`, `//nolint :xxx`, `//nolint: xxx`\n\n## Exclusion Presets\n\n```yaml\nlinters:\n  exclusions:\n    presets:\n      - comments              # Suppress exported-should-have-comment checks\n      - std-error-handling    # Suppress errcheck on stdout/stderr/Close/Flush\n      - common-false-positives # Suppress common gosec false positives\n      - legacy                # Suppress legacy govet/staticcheck patterns\n```\n\nNot enabled by default in v2 - you must opt in explicitly.\n\n`path-except` / `paths-except` are the inverses, letting a linter run *only* on matching files - \"Run some linter only for test files by excluding its issues for everything else. - path-except: `_test\\.go`\".\n\n## Output Formats\n\n`output.formats.text` is only one of nine. All can be written simultaneously, each to its own path:\n\n```yaml\noutput:\n  formats:\n    text:\n      path: stdout\n      print-linter-name: true\n      colors: true\n    sarif:\n      path: golangci-lint.sarif\n    junit-xml:\n      path: golangci-lint-report.xml\n```\n\nAvailable: `text`, `json`, `tab`, `html`, `checkstyle`, `code-climate`, `junit-xml`, `teamcity`, `sarif`.\n\n`sarif` is the path into GitHub code scanning - upload the file with `github/codeql-action/upload-sarif` and findings appear as annotations in the Security tab. `junit-xml` and `checkstyle` cover most other CI systems.\n\n## Incremental Adoption\n\nBeyond the Action's `only-new-issues`, the binary can restrict reporting to changed code:\n\n```bash\ngolangci-lint run --new-from-merge-base=main   # only issues absent from the merge base\ngolangci-lint run --new-from-rev=HEAD~1        # only issues introduced since a revision\ngolangci-lint run --new-from-patch=changes.patch\ngolangci-lint run --whole-files                # report all issues in a changed file, not just changed lines\ngolangci-lint run --enable-only=errcheck       # run exactly one linter, ignoring config\n```\n\nThe same knobs exist in config under `issues.new`, `issues.new-from-merge-base`, and `issues.new-from-rev`.\n\n## Module Plugins\n\nLinters not bundled with golangci-lint can be compiled into a custom binary. Define the build in `.custom-gcl.yml`, then \"Run the command `golangci-lint custom`\" to produce it:\n\n```yaml\n# .custom-gcl.yml\nversion: v2.14.0\nname: custom-golangci-lint\ndestination: ./bin\nplugins:\n  - module: github.com/example/my-linter\n    version: v1.0.0\n```\n\nThe resulting binary reads the same `.golangci.yml` and exposes the plugin's linters alongside the built-in set.\n\nModule plugins are one of two plugin systems. **Go plugins** (`.so` files loaded at runtime, built with `go build -buildmode=plugin`) are the other; they avoid the rebuild step but are fragile across toolchain versions and unsupported on Windows. Prefer module plugins unless you specifically need runtime loading.\n\n## Editor and Shell Integration\n\nBeyond the editor settings below, two integrations are easy to miss:\n\n- **`golangci-lint-langserver`** exposes the linter over LSP for NeoVim, Vim, and Emacs, so findings appear inline without a save-and-run cycle.\n- **`golangci-lint completion`** generates shell completion: \"Golangci-lint can generate Bash, fish, PowerShell, and Zsh completion files.\"\n\n## Other CI Systems\n\nThe GitHub Action is the best-supported path, but upstream documents GitLab CI and Buildkite recipes as well. The portable shape is the install script plus a cached `$GOLANGCI_LINT_CACHE`; use the `junit-xml` or `checkstyle` output format to surface findings natively in those systems.\n\n## Formatters Section (v2)\n\nFormatters are separate from linters in v2. They have their own `enable`, `settings`, and `exclusions`.\n\nAvailable formatters: `gci`, `gofmt`, `gofumpt`, `goimports`, `golines`, `swaggo` (added v2.2.0)\n\n```yaml\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  settings:\n    gofumpt:\n      # `extra.*` since v2.13.0. `extra-rules: true` is deprecated and also enables balance_calls.\n      extra:\n        group-params: true\n        clothe-returns: true\n        balance-calls: false\n    goimports:\n      local-prefixes:          # a list in v2 - a bare string fails `config verify`\n        - github.com/myorg/myrepo\n```\n\nRun: `golangci-lint fmt`, `golangci-lint fmt --diff`, or `golangci-lint fmt --diff-colored`.\n\nDo not pair `golangci-lint fmt` as the CI gate with a standalone `gofumpt -w` as the fixer - they can disagree on the same file, so the gate fails on code the fixer just formatted. Pick one for both roles.\n\n## GitHub Actions\n\nOfficial action: `golangci/golangci-lint-action@v9`\n\n```yaml\npermissions:\n  contents: read\n  pull-requests: read   # only needed with only-new-issues\n\njobs:\n  golangci:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version: stable\n      - uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.14\n          # only-new-issues: true  # For incremental adoption\n```\n\nKeep `version:` at or above the Go version `setup-go` resolves. With `go-version: stable` that is the newest Go release, so a pin left behind after a Go major bump breaks the job.\n\n`pull-requests: read` is what `only-new-issues` needs - the README's permissions block: \"Optional: allow read access to pull requests. Use with `only-new-issues` option.\" On `pull_request` and `push` the action fetches the diff from the GitHub API; on `merge_group` it uses `--new-from-rev` and needs `fetch-depth: 0` on checkout.\n\n**Config verification is built in.** Before `run`, the action runs `golangci-lint config verify` itself whenever it finds a config file (`src/run.ts`, `runVerify`): \"If the GitHub Action detects a configuration file, validation will be performed unless this option is set to `false`.\" A separate verify step in the workflow is redundant; `config verify` stays useful locally and in hooks.\n\nKey options:\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `version` | *(optional)* | e.g. `v2.14`, `v2.14.0`, or `latest`. Declared `required: false` in `action.yml` - omit it and the action resolves a default |\n| `version-file` | - | Read the version from `.golangci-lint-version` or `.tool-versions` |\n| `install-mode` | `binary` | `binary`, `goinstall`, or `none` (use a golangci-lint already on `PATH`, e.g. from mise). \"`goinstall` is not recommended\" |\n| `install-only` | false | Install the binary without running it |\n| `working-directory` | repo root | \"The golangci-lint working directory, useful for monorepos\" |\n| `only-new-issues` | false | Show only new issues on PRs (needs `pull-requests: read`) |\n| `verify` | true | Run `config verify` before `run` when a config file exists |\n| `experimental` | - | Comma-separated: `automatic-module-directories`, `no-run-logs-group` |\n| `cache-invalidation-interval` | 7 | Days before cache refresh |\n| `skip-save-cache` | false | Restore but don't save cache |\n\n**Monorepos and Go workspaces.** The README's pattern for a multi-module repo or a `go.work` workspace is one run per module. Its \"Go Workspace Example\" lists modules with `go list -m -json`, which returns every module in `go.work`, and fans out with a matrix:\n\n```yaml\njobs:\n  detect-modules:\n    runs-on: ubuntu-latest\n    outputs:\n      modules: ${{ steps.set-modules.outputs.modules }}\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version: stable\n      - id: set-modules\n        run: echo \"modules=$(go list -m -json | jq -s '.' | jq -c '[.[].Dir]')\" >> $GITHUB_OUTPUT\n\n  golangci-lint:\n    needs: detect-modules\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        modules: ${{ fromJSON(needs.detect-modules.outputs.modules) }}\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-go@v7\n        with:\n          go-version: stable\n      - uses: golangci/golangci-lint-action@v9\n        with:\n          version: v2.14\n          working-directory: ${{ matrix.modules }}\n```\n\nThe single-job alternative is `experimental: \"automatic-module-directories\"`, which \"will run golangci-lint in each module directory\" under `working-directory` (or the root). Caveats from the README: all runs share one cache key, \"The version detection will only work if the project has a single module\", and a custom build file must sit in the root (or `working-directory`).\n\n### Other CI pieces\n\n- **govulncheck:** `golang/govulncheck-action@v1` takes `go-version-file`, `cache-dependency-path`, `work-dir`, and `output-format` (`text`, `json`, `sarif`). Only `text` gates the job - \"Specifying the output format 'json' or 'sarif' will return success even if there are some vulnerabilities detected.\" Use `sarif` for the Security tab, `text` for a failing check. **`go-version-file` alone is silently ignored:** `go-version-input` defaults to `'stable'` and setup-go prefers `go-version` (\"Both go-version and go-version-file inputs are specified, only go-version will be used\"), so stdlib advisories are judged against the newest Go, not yours. Pass `go-version-input: ''` with it. The action also installs `govulncheck@latest`, so for a pinned scanner use the plain `go install ...@v1.8.0` step from SKILL.md.\n- **Keeping pins current:** \"Renovate can update both the action and the `golangci-lint` version it uses\" (action README) through its github-actions manager, which beats bumping the `version:` input by hand.\n- **just:** CI never runs the Justfile unless the job installs `just`. `extractions/setup-just@v4` (latest tag v4.0.0, input `just-version`) or `taiki-e/install-action@just`:\n\n```yaml\n- uses: extractions/setup-just@v4\n  with:\n    just-version: 1.58.0\n- run: just check\n```\n\n## Editor Integration\n\n**VS Code:**\n```json\n{\n  \"go.lintTool\": \"golangci-lint\",\n  \"go.lintFlags\": [\"--path-mode=abs\", \"--fast-only\"]\n}\n```\n\nFormat on save through golangci-lint itself, so the editor runs the same formatter set and settings as the `fmt --diff` gate (gopls' own `gofumpt` switch cannot select `extra.*` rules or run goimports):\n\n```json\n{\n  \"go.formatTool\": \"custom\",\n  \"go.alternateTools\": { \"customFormatter\": \"golangci-lint\" },\n  \"go.formatFlags\": [\"fmt\", \"--stdin\"]\n}\n```\n\n**GoLand:** Built-in support since 2025.1 for both v1 and v2.\n\n## v2 Migration from v1\n\nKey breaking changes in v2.0.0 (March 2025):\n\n- `version: \"2\"` required in config\n- `staticcheck`, `gosimple`, `stylecheck` merged into `staticcheck`\n- `linters-settings:` moved under `linters.settings:`\n- `issues.exclude-rules` moved to `linters.exclusions.rules`\n- Formatters moved to `formatters:` section\n- `disable-all: true` replaced by `default: none`\n- No exclusions by default (must use `presets:`)\n- Many deprecated linters removed (`deadcode`, `golint`, `varcheck`, etc.)\n\nRun `golangci-lint migrate` to auto-convert v1 configs.\n\n## Notable v2.x Additions\n\n| Version | Key Additions |\n|---------|--------------|\n| v2.1.0 | `funcorder` linter, colored diff for `fmt` |\n| v2.2.0 | `noinlineerr` linter, `wsl_v5` replaces deprecated `wsl` |\n| v2.4.0 | Go 1.25 support |\n| v2.5.0 | `godoclint`, `unqueryvet`, `iotamixing` linters |\n| v2.6.0 | `modernize` analyzer suite |\n| v2.9.0 | Go 1.26 support |\n| v2.11.0 | New gosec rules, revive `package-naming` (⚠️ breaking: package checks moved out of `var-naming`) |\n| v2.12.0 | `clickhouselint` linter, `gomodguard_v2` major bump, JSON schema embedded in the binary |\n| v2.13.0 | **Go 1.27 support**; `exhaustruct` deprecated in favour of `exhaustruct_v5`; gofumpt 0.11.0 with granular `extra.*` options; `govet-modernize` 0.49.0 |\n| v2.13.1 | Linter bug fixes |\n| v2.13.2 | Cache-entropy fix; linter deps bumped (`staticcheck` 0.8.1, `iface` 1.5.1, `unparam`); `canonicalheader` moved to a temporary fork. No config-schema change |\n| v2.14.0 | gofumpt 0.11.0 -> 0.12.0; \"fix: cache of facts reloading\" (#6810, fixes #6807); \"fix: ignore TextEdits outside the analyzed file\" (#6816); revive 1.15.0 -> 1.17.0 (new rules `marshal-receiver`, `multiline-if-init`, `use-slices-concat`); gosec 2.29.0 \"re-enable G407\"; bodyclose `//bodyclose:handled` directive; `exhaustruct_v5` option `allow-empty-blank-assignments`. No new linters (current release) |\n\n**Upgrading to v2.14.0 surfaces new findings on unchanged code.** With `revive: enable-all-rules: true` the three new revive rules switch on automatically, and gosec re-enables G407, so budget a cleanup pass (or `disable` those rules) when bumping. #6816 fixes `--fix` corrupting code when an analyzer splits one fix's edits across files - #6671, where modernize `atomictypes` appended `b.go`'s edit onto the end of `a.go`; the fix drops the out-of-file edits rather than applying them (\"Not the perfect fix, but good enough for now\"), so such a fix now lands only in the reported file. Build after `--fix` and finish the other files by hand.\n\nFile v0.6.0:references/gotestsum-reference.md\n\n# gotestsum Reference\n\nLatest: **v1.13.0** (September 2025; still current as of 2026-09). Module: `gotest.tools/gotestsum`. Requires Go 1.24+.\n\nA test runner that wraps `go test -json` with readable output, watch mode, JUnit XML, and rerun capabilities.\n\n## Installation\n\n```bash\ngo install gotest.tools/gotestsum@v1.13.0\n\n# Or run without installing\ngo run gotest.tools/gotestsum@v1.13.0\n\n# Homebrew\nbrew install gotestsum\n```\n\n## Basic Usage\n\n```bash\n# Run all tests (equivalent to: go test -json ./...)\ngotestsum\n\n# With format and race detection\ngotestsum --format testname -- -race ./...\n\n# Everything after -- is passed to go test\ngotestsum -- -tags=integration -count=1 ./...\n\n# Single package\ngotestsum -- ./internal/user\n\n# Specific test\ngotestsum -- -run TestMyFunc ./...\n\n# With coverage\ngotestsum -- -race -coverprofile=cover.out -covermode=atomic ./...\n```\n\n## Output Formats\n\nSet via `--format` flag or `GOTESTSUM_FORMAT` env var. Default: `pkgname`.\n\n| Format | Description |\n|--------|-------------|\n| `dots` | Print a character for each test |\n| `dots-v2` | One package per line |\n| `pkgname` | One line per package (default) |\n| `pkgname-and-test-fails` | One line per package + failed test output |\n| `testname` | One line per test and package |\n| `testdox` | Sentence for each test |\n| `github-actions` | testname with GitHub Actions log grouping |\n| `standard-quiet` | Standard `go test` format |\n| `standard-verbose` | Standard `go test -v` format |\n\n**Format icons** (`--format-icons` or `GOTESTSUM_FORMAT_ICONS`):\n- `default` - unicode (check, X)\n- `hivis` - high visibility unicode\n- `text` - PASS, SKIP, FAIL\n- `codicons` / `octicons` / `emoticons` - Nerd Fonts\n\nAdditional: `--format-hide-empty-pkg` hides packages with no tests.\n\n## Watch Mode\n\n```bash\n# Basic watch\ngotestsum --watch --format testname\n\n# With screen clearing (v1.13.0+)\ngotestsum --watch --watch-clear --format testname\n\n# Watch with chdir (multi-module repos)\ngotestsum --watch --watch-chdir\n```\n\n**Interactive keys in watch mode:**\n- `r` - rerun tests for previous event\n- `u` - rerun with `-update` flag (golden files)\n- `d` - debug with delve\n- `a` - run all tests (`./...`)\n- `l` - rescan directories for new `.go` files\n\n## JUnit XML Output\n\n```bash\n# Basic JUnit output\ngotestsum --junitfile unit-tests.xml\n\n# With CI-friendly format\ngotestsum --format github-actions --junitfile unit-tests.xml -- -race ./...\n\n# Customize naming\ngotestsum --junitfile unit-tests.xml \\\n  --junitfile-testsuite-name relative \\\n  --junitfile-testcase-classname short\n\n# Project name and clean output\ngotestsum --junitfile unit-tests.xml \\\n  --junitfile-project-name \"my-service\" \\\n  --junitfile-hide-empty-pkg \\\n  --junitfile-hide-skipped-tests\n```\n\nName format options (`--junitfile-testsuite-name`, `--junitfile-testcase-classname`):\n- `full` (default) - full package path\n- `relative` - relative to repo root\n- `short` - base package name\n\n## Rerunning Failed Tests\n\n```bash\n# Rerun failed tests up to 2 times\ngotestsum --rerun-fails --packages=\"./...\" -- -count=1\n\n# Custom retry count and threshold\ngotestsum --rerun-fails=3 \\\n  --rerun-fails-max-failures=5 \\\n  --packages=\"./...\" \\\n  -- -count=1\n\n# Rerun root test when subtests fail\ngotestsum --rerun-fails --rerun-fails-run-root-test --packages=\"./...\"\n\n# Abort rerun on data race (v1.12.3+)\ngotestsum --rerun-fails --rerun-fails-abort-on-data-race --packages=\"./...\"\n\n# Flags for the test binary (not go test) go after -args\ngotestsum --rerun-fails --packages=\"./...\" -- -count=2 -args -update-golden\n```\n\nWith `--rerun-fails`, \"if any of the `go test` args should be passed to the test binary, instead of `go test` itself, the `-args` flag must be used to separate the two groups of arguments.\"\n\n## Tools\n\n### Find Slowest Tests\n\n```bash\ngotestsum --format dots --jsonfile test.json ./...\ngotestsum tool slowest --jsonfile test.json --threshold 500ms\n```\n\n### Auto-skip Slow Tests\n\n```bash\ngo test -json -short ./... | gotestsum tool slowest --skip-stmt \"testing.Short\" --threshold 200ms\n```\n\n### CI Matrix Partitioning\n\n```bash\n# Partition tests across CI jobs based on timing data\necho -n \"matrix=\" >> $GITHUB_OUTPUT\ngo list ./... | gotestsum tool ci-matrix --timing-files ./*.log --partitions 4 >> $GITHUB_OUTPUT\n```\n\n## Custom Commands with `--raw-command`\n\n`--raw-command` tells gotestsum to run your command verbatim instead of prepending `go test -json`. The contract is strict on both streams:\n\n- stdout: \"The stdout produced by the script must only contain the `test2json` output, or `gotestsum` will fail.\" If the script cannot avoid it, \"you can use `--ignore-non-json-output-lines` (added in version 1.7.0) to ignore non-JSON lines and write them to `gotestsum`'s stderr instead.\"\n- stderr: \"Any stderr produced by the script will be considered an error (this behaviour is necessary because package build errors are only reported by writing to stderr, not the `test2json` stdout).\" Stderr written by the tests themselves is fine - it arrives inside the `test2json` stdout.\n\nSo script chatter must be silenced or printed to stdout under `--ignore-non-json-output-lines` - never sent to stderr.\n\nThis is how you run an already-compiled test binary - useful for cross-compiled or long-lived test binaries you do not want to rebuild:\n\n```bash\ngotestsum --raw-command -- go tool test2json -t -p pkgname ./binary.test -test.v\n```\n\n`-p` supplies the package name that `test2json` cannot infer from a bare binary, and `-t` adds timestamps.\n\n## Post-Run Commands\n\n```bash\n# Desktop notifications\ngo install gotest.tools/gotestsum/contrib/notify@v1.13.0\ngotestsum --post-run-command notify\n\n# Print slowest tests after run\ngotestsum --jsonfile tmp.json \\\n  --post-run-command \"bash -c 'gotestsum tool slowest --num 10 --jsonfile tmp.json'\"\n```\n\nPost-run environment variables:\n- `GOTESTSUM_ELAPSED` - test run time\n- `TESTS_TOTAL`, `TESTS_FAILED`, `TESTS_SKIPPED`, `TESTS_ERRORS`\n\n## All CLI Flags\n\n```\n--format, -f string          Output format (default \"pkgname\")\n--format-hide-empty-pkg      Hide empty packages\n--format-icons string        Icon set\n--raw-command                Don't prepend 'go test -json'\n--no-color                   Disable color (auto-detected in CI)\n--max-fails int              Stop after N failures\n--jsonfile string            Write all TestEvents to file\n--jsonfile-timing-events string  Write only pass/skip/fail events to the file\n--junitfile string           Write JUnit XML\n--junitfile-testsuite-name   Name format: full|relative|short\n--junitfile-testcase-classname  Classname format: full|relative|short\n--junitfile-project-name     Project name in XML\n--junitfile-hide-empty-pkg   Omit empty packages in XML\n--junitfile-hide-skipped-tests  Omit skipped tests in XML\n--hide-summary string        Hide: skipped,failed,errors,output,all\n--rerun-fails int            Rerun failed tests (default max 2)\n--rerun-fails-max-failures   Skip rerun if initial failures > N (default 10)\n--rerun-fails-run-root-test  Rerun root test case for subtest failures\n--rerun-fails-abort-on-data-race  Stop rerun on data race\n--rerun-fails-report string  Write a report of the reruns to the file\n--ignore-non-json-output-lines  Send non-JSON stdout lines to stderr\n--watch                      Watch .go files and rerun\n--watch-chdir                cd to modified file's dir\n--watch-clear                Clear screen on rerun\n--packages list              Space-separated package list\n--post-run-command command   Run after tests complete\n--debug                      Enable debug logging\n--version                    Show version\n```\n\n## Environment Variables\n\n| Variable | Purpose |\n|----------|---------|\n| `GOTESTSUM_FORMAT` | Default output format |\n| `GOTESTSUM_FORMAT_ICONS` | Icon set |\n| `GOTESTSUM_JUNITFILE` | JUnit output path |\n| `GOTESTSUM_JUNITFILE_PROJECT_NAME` | Project name in JUnit |\n| `GOTESTSUM_JSONFILE` | JSON output path |\n| `GOTESTSUM_JSONFILE_TIMING_EVENTS` | Path for the timing-events-only JSON file (`--jsonfile-timing-events`) |\n| `GOTESTSUM_JUNIT_HIDE_EMPTY_PKG` | Omit packages with no tests from JUnit (`--junitfile-hide-empty-pkg`) |\n| `GOTESTSUM_JUNIT_HIDE_SKIPPED_TESTS` | Omit skipped tests from JUnit (`--junitfile-hide-skipped-tests`) |\n| `TEST_DIRECTORY` | Default test directory (instead of `./...`) |\n| `GOVERSION` | Go version for JUnit XML when `go` is not on PATH |\n\n## Justfile Recipes\n\n```just\n# Run all tests\ntest:\n    gotestsum --format testname -- -race ./...\n\n# Tests with coverage\ntest-cov:\n    gotestsum --format testname -- -race -coverprofile=cover.out -covermode=atomic ./...\n    go tool cover -func=cover.out\n\n# CI output with JUnit XML\ntest-ci:\n    gotestsum --format github-actions \\\n      --junitfile unit-tests.xml \\\n      --junitfile-hide-empty-pkg \\\n      -- -race -count=1 ./...\n\n# Watch mode\ntest-watch:\n    gotestsum --watch --watch-clear --format testname\n\n# Rerun flaky tests\ntest-flaky:\n    gotestsum --format testname \\\n      --rerun-fails=3 \\\n      --rerun-fails-max-failures=5 \\\n      --packages=\"./...\" -- -count=1\n```\n\n## Recent Changes\n\n| Version | Date | Key Changes |\n|---------|------|-------------|\n| v1.13.0 | Sep 2025 | `--watch-clear` flag, Go test attributes support (`t.Attr`, Go 1.25+) |\n| v1.12.3 | Jun 2025 | `--rerun-fails-abort-on-data-race` flag |\n| v1.12.2 | May 2025 | `--junitfile-hide-skipped-tests` flag |\n| v1.12.1 | Mar 2025 | Go 1.24 compatibility, JUnit `skipped` attribute |\n| v1.12.0 | May 2024 | `--format-icons` flag with Nerd Fonts |\n\nFile v0.6.0:references/justfile-reference.md\n\n# Justfile Reference for Go Projects\n\n`just` is a command runner (not a build system). It runs recipes defined in a `Justfile`. Latest: **1.58.0** (2026-08-03).\n\n## Installation\n\n```bash\n# macOS\nbrew install just\n\n# Cargo\ncargo install just\n\n# Pre-built binaries\n# https://github.com/casey/just/releases\n```\n\n## Core Syntax\n\n```just\nset shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]   # Strict bash: errexit, undefined vars, pipefail\nset dotenv-load                                    # Load .env file\n\n# Recipe with doc comment\nrecipe-name:\n    command1\n    command2\n\n# Recipe with arguments\nbuild target=\"./cmd/myapp\":\n    go build -o myapp {{ target }}\n\n# Recipe with dependencies\ncheck: fmt-check lint test\n    @echo \"All checks passed\"\n```\n\n**Key rules:**\n- Indent recipe bodies consistently - **either** tabs or spaces works (unlike Makefiles, which demand tabs), but the indentation must be uniform within a recipe. Nothing enforces a project-wide choice: `set indentation` only controls what the formatter writes - \"Set recipe body indentation used when formatting with `--fmt` or `--dump`.\" The templates in this reference use spaces\n- Each line runs in a separate shell (use `&&` or `\\` to chain)\n- `@` prefix suppresses command echo\n- `#` comments above a recipe become its doc string\n\n## Variables\n\n```just\nbinary := \"myapp\"                              # Simple\nversion := `git describe --tags --always`      # Backtick (shell command)\nexport DATABASE_URL := env(\"DATABASE_URL\", \"\") # Environment with default\n```\n\n## Parameters\n\n```just\n# Required parameter\nmigrate-create name:\n    migrate create -ext sql -dir migrations -seq {{ name }}\n\n# Default parameter\ntest *args=\"./...\":\n    gotestsum --format testname -- -race {{ args }}\n\n# Variadic\nrun *args:\n    go run ./cmd/myapp {{ args }}\n```\n\n## Dependencies\n\n```just\n# Prior dependencies (run before recipe)\ncoverage: test-cov\n    go tool cover -html=coverage.out\n\n# With arguments\ndeploy env: (build env)\n    ./scripts/deploy.sh {{ env }}\n```\n\n## Recipe Attributes\n\n```just\n# Group recipes in --list output\n[group('quality')]\nlint:\n    golangci-lint run ./...\n\n# Hide from --list\n[private]\ndefault:\n    @just --list --unsorted\n\n# Require confirmation before running\n[confirm(\"Drop all tables?\")]\ndb-drop:\n    migrate -path migrations -database \"$DATABASE_URL\" drop -f\n\n# Platform-specific\n[linux]\ninstall:\n    sudo cp myapp /usr/local/bin/\n\n[macos]\ninstall:\n    cp myapp /usr/local/bin/\n\n# Documented recipe (alternative to comment)\n[doc(\"Run all tests with race detection\")]\ntest:\n    gotestsum --format testname -- -race ./...\n\n# Run the recipe from a fixed directory, whatever the invocation dir\n[working-directory('backend')]\nmigrate-up:\n    migrate -path migrations -database \"$DATABASE_URL\" up\n\n# Set an env var for this recipe only\n[env('CGO_ENABLED', '0')]\nbuild-static:\n    go build -o myapp ./cmd/myapp\n\n# Run this recipe's dependencies concurrently\n[parallel]\ncheck-all: lint test vuln\n\n# Print a timestamp before each command\n[timestamp]\nslow-task:\n    go test -run TestBigIntegration ./...\n\n# Treat the body as a script for one interpreter (no per-line shells)\n[script('bash', '-euo', 'pipefail')]   # no `-c`: just passes the script's *path* as the last argument\nrelease:\n    VERSION=$(git describe --tags --always)\n    goreleaser release --clean\n```\n\n### Recipe flags with `[arg(...)]`\n\nTurns positional parameters into real command-line options - \"Require values of argument `ARG` to be passed as `--LONG` option.\"\n\n```just\n[arg('env', long='environment', short='e')]\ndeploy env='staging':\n    ./scripts/deploy.sh {{ env }}\n```\n\nInvoke as `just deploy --environment prod` instead of `just deploy prod`.\n\n## Settings\n\n```just\nset shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]   # Shell and flags\nset dotenv-load                                    # Auto-load .env\nset export                        # Export all variables as env vars\nset quiet                         # Suppress command echo by default\nset positional-arguments          # Pass args as $1, $2, etc.\n\nset dotenv-path := \".env.local\"   # Load a specific env file\nset dotenv-required               # Fail if the env file is missing\nset dotenv-override               # .env wins over the ambient environment\nset working-directory := \"backend\"  # Default dir for every recipe\nset indentation := \"    \"         # Indentation --fmt/--dump write (validates nothing)\nset minimum-version := \"1.58.0\"   # Error if `just` is older than this\nset script-interpreter := [\"bash\", \"-euo\", \"pipefail\"]  # Default for [script] recipes\nset fallback                      # Search parent directories for a recipe\nset no-exit-message               # Suppress just's own error line on failure\nset dotenv-command := 'sops -d .enc.env'  # Run a command, load its output as the env file\nset default-script                # Recipes default to script mode instead of shell mode\nset default-list                  # Bare `just` lists recipes instead of running the default\n```\n\nThis is a catalog, not a copy-pasteable header - `dotenv-command` and `dotenv-load` are mutually exclusive, and `just` rejects a file setting both.\n\n`set default-list` (1.52.0) - \"List recipes instead of running the default recipe.\" - can replace the `[private] default: @just --list --unsorted` recipe used below, but the setting has no `--unsorted` counterpart, so a bare `just` lists alphabetically. Keep the recipe when declaration order matters.\n\nWrite boolean settings bare (`set dotenv-load`, `set export`): `just --fmt` rewrites `set x := true` to that form, so a Justfile written the long way fails `just --fmt --check`.\n\n`set minimum-version` (1.55.0+) is worth adding to any Justfile that uses recent attributes: without it, an older `just` fails with a confusing parse error instead of a version message. A `just` older than 1.55.0 does not know the setting either, so it still gets a parse error - just a more obvious one.\n\n## Shebang Recipes\n\nRun a recipe with a different interpreter:\n\n```just\n# Python script\n[group('tools')]\ngenerate-docs:\n    #!/usr/bin/env python3\n    import json\n    with open(\"api.json\") as f:\n        spec = json.load(f)\n    print(f\"Found {len(spec['paths'])} endpoints\")\n\n# Bash with strict mode\n[group('ci')]\nrelease:\n    #!/usr/bin/env bash\n    set -euo pipefail\n    VERSION=$(git describe --tags --always)\n    echo \"Releasing $VERSION\"\n    goreleaser release --clean\n```\n\n## Conditional Logic\n\n```just\n# Ternary\ntest-cmd := if env(\"CI\", \"\") != \"\" { \"gotestsum --format github-actions\" } else { \"gotestsum --format testname\" }\n\ntest:\n    {{ test-cmd }} -- -race ./...\n\n# In-recipe conditionals (bash)\ndeploy env:\n    #!/usr/bin/env bash\n    if [ \"{{ env }}\" = \"prod\" ]; then\n        echo \"Deploying to production\"\n    else\n        echo \"Deploying to {{ env }}\"\n    fi\n```\n\n## Built-in Functions\n\n| Function | Description |\n|----------|-------------|\n| `env(\"KEY\", \"default\")` | Read environment variable |\n| `home_directory()` | User home directory |\n| `os()` | Operating system |\n| `arch()` | CPU architecture |\n| `justfile_directory()` | Directory containing the Justfile |\n| `invocation_directory()` | Directory where `just` was invoked |\n| `trim(s)` | Trim whitespace |\n| `replace(s, from, to)` | String replacement |\n| `uppercase(s)` / `lowercase(s)` | Case conversion |\n\n## Complete Go Project Justfile\n\n```just\nset shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]\nset dotenv-load\n\nexport PATH := home_directory() + \"/go/bin:\" + env('PATH')\n\nbinary := \"myapp\"\n\n[private]\ndefault:\n    @just --list --unsorted\n\n# ── Code Quality ──────────────────────────────────────────\n\n# Format all Go code\n[group('quality')]\nfmt:\n    golangci-lint fmt ./...\n\n# Check formatting (CI-safe, non-zero exit on diff)\n# Gate with the same tool that fixes - see the two-formatter footgun in SKILL.md\n[group('quality')]\nfmt-check:\n    golangci-lint fmt --diff ./...\n\n# Run linter\n[group('quality')]\nlint:\n    golangci-lint run ./...\n\n# Run linter with auto-fix\n[group('quality')]\nlint-fix:\n    golangci-lint run --fix ./...\n\n# Run vulnerability check\n[group('quality')]\nvuln:\n    govulncheck ./...\n\n# ── Testing ───────────────────────────────────────────────\n\n# Run all tests with race detection\n[group('test')]\ntest *args=\"./...\":\n    gotestsum --format testname -- -race {{ args }}\n\n# Run tests with coverage\n[group('test')]\ntest-cov:\n    gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...\n    go tool cover -func=coverage.out\n\n# Open coverage report in browser\n[group('test')]\ncoverage: test-cov\n    go tool cover -html=coverage.out\n\n# Run integration tests\n[group('test')]\ntest-integration:\n    gotestsum --format testname -- -race -tags=integration ./...\n\n# Watch tests during development\n[group('test')]\ntest-watch:\n    gotestsum --watch --watch-clear --format testname\n\n# Run benchmarks\n[group('test')]\nbench:\n    go test -bench=. -benchmem ./...\n\n# ── Build ─────────────────────────────────────────────────\n\n# Build the binary\n[group('build')]\nbuild:\n    go build -o {{ binary }} ./cmd/{{ binary }}\n\n# Build optimized release binary\n[group('build')]\nbuild-release:\n    CGO_ENABLED=0 go build -trimpath -ldflags=\"-s -w\" -o {{ binary }} ./cmd/{{ binary }}\n\n# ── Dependencies ──────────────────────────────────────────\n\n# Tidy and verify modules\n[group('deps')]\ntidy:\n    go mod tidy\n    go mod verify\n\n# Fail if go.mod/go.sum are not tidy (CI-safe, modifies nothing)\n[group('deps')]\ntidy-check:\n    go mod tidy -diff\n\n# Run code generators\n[group('deps')]\ngenerate:\n    go generate ./...\n\n# Update all dependencies\n[group('deps')]\nupdate-deps:\n    go get -u ./...\n    go mod tidy\n\n# ── Database ──────────────────────────────────────────────\n\n# Apply all pending migrations\n[group('db')]\nmigrate-up:\n    migrate -path migrations -database \"$DATABASE_URL\" up\n\n# Revert last migration\n[group('db')]\nmigrate-down:\n    migrate -path migrations -database \"$DATABASE_URL\" down 1\n\n# Create a new migration\n[group('db')]\nmigrate-create name:\n    migrate create -ext sql -dir migrations -seq {{ name }}\n\n# Show migration version\n[group('db')]\nmigrate-version:\n    migrate -path migrations -database \"$DATABASE_URL\" version\n\n# ── CI ────────────────────────────────────────────────────\n\n# Full CI gate\n[group('ci')]\ncheck: fmt-check lint test\n    @echo \"All checks passed\"\n\n# CI test output with JUnit XML\n[group('ci')]\ntest-ci:\n    gotestsum --format github-actions \\\n      --junitfile unit-tests.xml \\\n      --junitfile-hide-empty-pkg \\\n      -- -race -count=1 ./...\n\n# Clean build artifacts\n[group('ci')]\nclean:\n    go clean\n    rm -f {{ binary }} coverage.out unit-tests.xml\n```\n\n`tidy-check` is the gate form of `tidy` - per `go help mod tidy`, \"The -diff flag causes tidy not to modify go.mod or go.sum but instead print the necessary changes as a unified diff. It exits with a non-zero code if the diff is not empty.\"\n\n## Lefthook Integration\n\nLefthook can call `just` recipes in hooks:\n\n```yaml\n# lefthook.yml\npre-commit:\n  commands:\n    fmt:\n      run: just fmt\n    lint:\n      glob: \"*.go\"\n      run: just lint\n\npre-push:\n  commands:\n    check:\n      run: just check\n```\n\nNote the direction of the dependency: git invokes the hook binary directly, so lefthook must be installed and its config must sit at the repo root or in `.config/` for any of this to fire. A `just` recipe cannot rescue a misplaced `lefthook.yml`.\n\nUseful in a `check` recipe:\n\n```just\n[group('ci')]\nhooks-check:\n    lefthook validate     # config is well-formed\n    lefthook dump         # print the merged effective config\n```\n\n## Importing Recipes\n\nSplit large Justfiles:\n\n```just\n# Justfile\nimport 'just/db.just'\nimport 'just/docker.just'\n```\n\n`import` splices recipes into the current namespace; `mod` keeps them namespaced, so recipes are invoked as `just db migrate-up`:\n\n```just\nmod db 'just/db.just'\nmod docker\n```\n\n```just\n# just/db.just\n[group('db')]\nmigrate-up:\n    migrate -path migrations -database \"$DATABASE_URL\" up\n```\n\n## Running\n\n```bash\njust                   # Run default recipe (list all)\njust test              # Run specific recipe\njust test ./pkg/...    # Recipe with argument\njust --list            # List all recipes\njust --list --unsorted # List in file order\njust --summary         # One-line summary of each recipe\njust --evaluate        # Show all variable values\njust --dry-run test    # Show what would run\njust -f path/Justfile  # Use specific Justfile\njust --fmt             # Format the Justfile in place\njust --fmt --check     # Exit non-zero if the Justfile is not formatted (CI gate)\njust --jobs 4          # Cap parallelism for [parallel] dependencies\n```\n\n`just --fmt --check` is a natural addition to the `check` recipe - \"Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly.\" Gate on it only with one pinned `just` version for developers and CI: \"Note that formatting is not covered by any backwards compatibility guarantee and is subject to change from time to time.\" 1.58.0 itself changed the output (\"Surround interoplation expressions with spaces when formatting\", sic), so the same file can pass the check under one version and fail it under the next. `set minimum-version` is only a floor, not a pin.\n\n## Go Tooling Traps in Recipes\n\nThree ways a recipe that reads correctly still fails:\n\n**`go tool` is scoped to the current directory's module.** Per `go help tool`, \"additional tools may be defined in the go.mod of the current module\" - so in a monorepo, a recipe invoked from the repo root fails with `go: no such tool \"golangci-lint\"` even though the tool is tracked in the submodule. Pin the directory on the recipe:\n\n```just\n[group('quality')]\n[working-directory('services/api')]\nlint:\n    go tool golangci-lint run ./...\n```\n\n**Fail fast on a missing tool with `require()`** (1.39.0) - it returns the executable's full path \"or halt[s] with an error if no executable with `name` exists\". Call it inside the recipe body, `{{ require(\"golangci-lint\") }} run ./...`, so only that recipe fails; a top-level `tool := require(...)` assignment is evaluated for every recipe. The error names the missing binary instead of a bare `exit 127`.\n\n**A version-manager shim is a fourth install path**, alongside binary, Homebrew, and `go install`. If a tool is on PATH via mise, asdf, or similar and no version is pinned for the project, the recipe fails inside the shim rather than in the tool - `mise ERROR No version is set for shim: golangci-lint` - which reads like a Justfile bug. Pin the tool version in the version manager's config, or call an absolute path.\n\n**`GOFLAGS=-trimpath` lets worktrees share one warm cache.** `-trimpath` \"remove[s] all file system paths from the resulting executable\", which also makes the build and test cache keys path-independent. Without it, every git worktree recompiles the whole dependency tree (with `-race`, expensively) because its absolute paths differ:\n\n```just\nexport GOFLAGS := \"-trimpath\"\n```\n\nSet it at the top of the Justfile so `build`, `test`, and the linter's own package loading all share cache entries across worktrees.\n\n## Tips\n\n- Use `set shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]` to catch command failures, undefined variables, and broken pipelines\n- Group related recipes with `[group('name')]` for organized `--list` output\n- Use `[private]` for helper recipes that shouldn't appear in `--list`\n- `set dotenv-load` loads `.env` automatically - no separate tooling needed\n- `export PATH` to include `$(go env GOPATH)/bin` so Go-installed tools are always available\n- Prefer `just` over `make` for Go projects: no `.PHONY`, better variable handling, cross-platform, readable syntax\n\n## Beyond the Basics\n\nSurface worth knowing about, none of it needed for the Justfile above:\n\n| Feature | What it does |\n|---------|--------------|\n| Agent skill | just ships its own: \"A skill for agents is available in [skills/just] and may be installed manually or with `npx skills add casey/just --global`\" |\n| `just-lsp` / `just-mcp` | An LSP server, and an MCP adapter - \"just-mcp provides a model context protocol adapter to allow LLMs to query the contents of justfiles and run recipes\" |\n| `[cache]` (1.54.0) | \"Skip recipe invocations when a matching entry exists in the cache.\" Needs `set unstable` - \"The `[cache]` attribute may only be used with script recipes and is currently unstable.\" |\n| `set lists` (1.53.0) | \"Values may be lists of strings instead of strings. Currently unstable.\" |\n| `set guards` / `set lazy` (1.47.0) | Stable settings: `guards` - \"Enable the `?` guard sigil on recipe lines.\"; `lazy` - \"Don't evaluate unused variables.\" |\n| Remote and markdown justfiles | Run recipes from a URL, or keep them in fenced code blocks inside a Markdown file |\n| Global / user justfiles | A personal recipe set available from any directory |\n| `--choose` / `--man` / `--dump` | Interactive recipe picker, a generated man page, and a machine-readable dump |\n| `[continue(SIGNALS)]` | \"Continue execution normally if a command is interrupted by any of `SIGNALS` and exits successfully. Defaults to `SIGINT`\" |\n| User-defined functions (1.49.0) | Reusable expression-level helpers, distinct from recipes. Needs `set unstable` - \"User-defined functions are currently unstable.\" |\n| `[metadata]`, `[extension]`, `[no-cd]`, `[exit-message]`, `[default]` | Further recipe attributes |\n\nFile v0.6.0:references/lefthook-reference.md\n\n# Lefthook Reference\n\nLatest: **v2.1.17** (2026-10-05). Single Go binary, no runtime dependency. `go install github.com/evilmartians/lefthook/v2@v2.1.17` needs Go 1.26+; Homebrew, npm, and the GitHub release binaries avoid that floor.\n\nConfig is discovered at the repo root or in `.config/`, and read fresh on every hook run - \"Reinstall is not required when you modify `lefthook.yml`, the configuration file is read every time a git hook is run.\" Only adding or removing a *hook section* requires `lefthook install`.\n\n## v2.1.15 - v2.1.17 Notes\n\n- **The shim fix needs a reinstall.** \"fix: quote paths in the generated hook shim\" ([#1509](https://github.com/evilmartians/lefthook/pull/1509)) shell-escapes the lefthook binary path baked into `.git/hooks/<hook>` (`internal/templates/hook.tmpl:36-38`). Upgrading the binary alone changes nothing; the fix lands only once `lefthook install` rewrites the shims.\n- **Forced colours reach your jobs.** \"feat: propagate forced colors to hook commands via CLICOLOR_FORCE\" ([#1547](https://github.com/evilmartians/lefthook/pull/1547)): with colours explicitly on, lefthook sets `CLICOLOR_FORCE=1` for the commands it runs - \"An existing `CLICOLOR_FORCE` is never overwritten.\"\n- **Upgrade past v2.1.15 if lefthook is in your `go.mod`.** v2.1.15 pins `golang.org/x/mod v0.37.0` and `golang.org/x/text v0.38.0`, which carry [GO-2026-6179](https://pkg.go.dev/vuln/GO-2026-6179) and [GO-2026-6180](https://pkg.go.dev/vuln/GO-2026-6180) (x/mod, fixed in 0.40.0) and [GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970) (x/text, fixed in 0.39.0), so a lefthook tracked with `go get -tool` pulls them into your module graph and module-level scanners (`govulncheck -scan module`, Dependabot, osv-scanner) flag it. v2.1.16 fixes this - \"deps: bump mod and text deps to resolve CVEs\" ([#1560](https://github.com/evilmartians/lefthook/pull/1560)) - moving to x/mod v0.41.0 and x/text v0.42.0.\n- **A conflicting re-apply no longer wipes unrelated files** (v2.1.16) - \"fix: preserve unrelated unstaged changes on conflict\" ([#1483](https://github.com/evilmartians/lefthook/pull/1483)). See the worktree hazard under `stage_fixed`.\n- **`{push_files}` on a first push** (v2.1.17) - \"fix: diff push files against the merge base with the default branch\" ([#1565](https://github.com/evilmartians/lefthook/pull/1565)). Before, a branch with no `@{push}` used a two-dot diff, so files changed only upstream after branching showed up in `{push_files}` and woke `glob`-gated pre-push jobs.\n- **Staged submodules no longer break `fail_on_changes`** (v2.1.17) - \"fix: don't hash submodules when checking fail_on_changes\" ([#1566](https://github.com/evilmartians/lefthook/pull/1566)); before, the hook failed before any job ran.\n\n## Job Filtering (the part that silently skips work)\n\n**`glob` gates the job even when `run` has no file template.** This is the trap. The obvious reading is that `glob` only filters the list passed to `{staged_files}`, but the docs are explicit:\n\n> If you've specified `glob` but don't have a files template in `run` option, lefthook will check `{staged_files}` for `pre-commit` hook and `{push_files}` for `pre-push` hook and apply filtering. If no files left, the command will be skipped.\n\nSo a whole-project job carrying a glob quietly does nothing on commits that touch no matching file:\n\n```yaml\npre-commit:\n  commands:\n    # WRONG when you want this to always run: skipped on a docs-only commit\n    typecheck:\n      glob: \"*.go\"\n      run: go build ./...\n```\n\n**Rule of thumb: a job that checks the project carries no `glob`; a job that consumes `{staged_files}` keeps one.** For `go mod tidy` the glob is right - a commit touching no `.go`, `.mod`, or `.sum` file genuinely h\n\nArchive v0.5.0: 12 files, 71080 bytes\n\nFiles: CHANGELOG.md (13338b), LICENSE.txt (9157b), references/go-migrate-reference.md (13086b), references/go-testing-reference.md (22616b), references/gofumpt-reference.md (11190b), references/golangci-lint-reference.md (28394b), references/gotestsum-reference.md (9171b), references/justfile-reference.md (17160b), references/lefthook-reference.md (15931b), skill-card.md (2202b), SKILL.md (29352b), _meta.json (125b)\n\nArchive v0.4.1: 12 files, 60203 bytes\n\nFiles: CHANGELOG.md (10204b), LICENSE.txt (9157b), references/go-migrate-reference.md (11985b), references/go-testing-reference.md (18535b), references/gofumpt-reference.md (10475b), references/golangci-lint-reference.md (21286b), references/gotestsum-reference.md (8258b), references/justfile-reference.md (15539b), references/lefthook-reference.md (10947b), skill-card.md (2924b), SKILL.md (24809b), _meta.json (125b)\n\nArchive v0.4.0: 12 files, 58252 bytes\n\nFiles: CHANGELOG.md (8606b), LICENSE.txt (9157b), references/go-migrate-reference.md (11985b), references/go-testing-reference.md (18535b), references/gofumpt-reference.md (10475b), references/golangci-lint-reference.md (21286b), references/gotestsum-reference.md (8258b), references/justfile-reference.md (15539b), references/lefthook-reference.md (7789b), skill-card.md (2761b), SKILL.md (24809b), _meta.json (125b)\n\nArchive v0.3.1: 11 files, 45013 bytes\n\nFiles: CHANGELOG.md (3788b), LICENSE.txt (9157b), references/go-migrate-reference.md (10668b), references/go-testing-reference.md (18141b), references/gofumpt-reference.md (8768b), references/golangci-lint-reference.md (17043b), references/gotestsum-reference.md (7619b), references/justfile-reference.md (12303b), skill-card.md (2868b), SKILL.md (18694b), _meta.json (125b)\n\nArchive v0.3.0: 11 files, 44817 bytes\n\nFiles: CHANGELOG.md (3687b), LICENSE.txt (9157b), references/go-migrate-reference.md (10668b), references/go-testing-reference.md (18141b), references/gofumpt-reference.md (8768b), references/golangci-lint-reference.md (17043b), references/gotestsum-reference.md (7619b), references/justfile-reference.md (12303b), skill-card.md (2490b), SKILL.md (18767b), _meta.json (125b)\n\nArchive v0.2.4: 10 files, 33123 bytes\n\nFiles: LICENSE.txt (9157b), references/go-migrate-reference.md (9515b), references/go-testing-reference.md (14794b), references/gofumpt-reference.md (6350b), references/golangci-lint-reference.md (10933b), references/gotestsum-reference.md (7354b), references/justfile-reference.md (9271b), skill-card.md (2906b), SKILL.md (12242b), _meta.json (125b)\n\nArchive v0.2.3: 10 files, 33083 bytes\n\nFiles: LICENSE.txt (9157b), references/go-migrate-reference.md (9515b), references/go-testing-reference.md (14794b), references/gofumpt-reference.md (6350b), references/golangci-lint-reference.md (10933b), references/gotestsum-reference.md (7354b), references/justfile-reference.md (9271b), skill-card.md (2819b), SKILL.md (12160b), _meta.json (125b)\n\nArchive v0.2.2: 10 files, 33111 bytes\n\nFiles: LICENSE.txt (9157b), references/go-migrate-reference.md (9515b), references/go-testing-reference.md (14794b), references/gofumpt-reference.md (6350b), references/golangci-lint-reference.md (10933b), references/gotestsum-reference.md (7354b), references/justfile-reference.md (9271b), skill-card.md (2928b), SKILL.md (12430b), _meta.json (125b)\n\nArchive v0.2.1: 10 files, 33010 bytes\n\nFiles: LICENSE.txt (9157b), references/go-migrate-reference.md (9515b), references/go-testing-reference.md (14794b), references/gofumpt-reference.md (6350b), references/golangci-lint-reference.md (10933b), references/gotestsum-reference.md (7354b), references/justfile-reference.md (9271b), skill-card.md (2852b), SKILL.md (12169b), _meta.json (125b)","readmeExcerpt":"Skill: go-dev Owner: tenequm Summary: Opinionated Go setup with golangci-lint v2, gofumpt, gotestsum, golang-migrate, and just. Use when starting a Go project, configuring lint, format, test, coverage or CI, writing a Justfile, wiring migrations, or leaving a Makefile workflow. Tags: latest:0.6.0 Version history: v0.6.0 | 2026-10-06T12:07:45.096Z | user Updated go-dev from 0.5.0 to 0.6.0. Changes: - modified CHANGELO","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0"},{"language":"bash","snippet":"# 1. Create module\nmkdir myapp && cd myapp\ngo mod init github.com/yourorg/myapp\n\n# 2. Scaffold directories\nmkdir -p cmd/myapp internal migrations\n\n# 3. Install golangci-lint as a binary, not as a module tool (see note below)\ncurl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.14.0\n\n# 4. Track the rest in go.mod (Go 1.24+ tool directive). Pin versions - never @latest,\n#    which recompiles the tool on every CI run and drifts between machines.\ngo get -tool mvdan.cc/gofumpt@v0.12.0\ngo get -tool gotest.tools/gotestsum@v1.13.0\n\n# golang-migrate needs a build tag, so install it directly\ngo install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.20.1\n\n# 5. Create config files (templates below)\n# 6. Run: just check"},{"language":"yaml","snippet":"version: \"2\"\n\nrun:\n  timeout: 5m\n  build-tags:\n    - integration   # otherwise files behind the Justfile's integration tag are never linted\n\nlinters:\n  default: standard\n  enable:\n    - bodyclose\n    - copyloopvar\n    - dupl\n    - durationcheck\n    - err113\n    - errname\n    - errorlint\n    - exhaustive\n    - exptostd\n    - fatcontext\n    - goconst\n    - gocritic\n    - gosec\n    - intrange\n    - misspell\n    - modernize\n    - musttag\n    - nakedret\n    - nestif\n    - nilerr\n    - noctx\n    - nolintlint\n    - nonamedreturns\n    - perfsprint\n    - prealloc\n    - revive\n    - sqlclosecheck\n    - testifylint\n    - thelper\n    - unconvert\n    - unparam\n    - usestdlibvars\n    - usetesting\n    - wastedassign\n    - whitespace\n    - wrapcheck\n  settings:\n    govet:\n      enable:\n        - shadow\n    gocritic:\n      enabled-checks:\n        - nestingReduce\n    revive:\n      enable-all-rules: true\n      rules:\n        # enable-all-rules turns on `unhandled-error`, which flags `fmt.Println` in main.\n        # Under enable-all-rules a rule's `arguments` are ignored (the rule registers\n        # twice), so an allowlist does not work here - only `disabled` takes effect.\n        - name: unhandled-error\n          disabled: true\n    errcheck:\n      check-type-assertions: true\n  exclusions:\n    generated: strict\n    presets:\n      - comments\n      - std-error-handling\n      - common-false-positives\n    rules:\n      - path: _test\\.go\n        linters:\n          - errcheck\n          - dupl\n          - gosec\n          - wrapcheck\n\nformatters:\n  enable:\n    - gofumpt\n    - goimports\n  settings:\n    gofumpt:\n      # Select rules individually. `extra-rules: true` is deprecated, and it also\n      # switches on `balance_calls`, which gofumpt itself demoted as controversial.\n      extra:\n        group-params: true\n        clothe-returns: true\n        balance-calls: false\n  exclusions:\n    generated: strict\n    paths:\n      - vendor/\n\noutput:\n  formats:\n    text:\n      path: stdout\n      print-l"},{"language":"just","snippet":"set shell := [\"bash\", \"-euo\", \"pipefail\", \"-c\"]\nset dotenv-load\n\nbinary := \"myapp\"\n\n[private]\ndefault:\n    @just --list --unsorted\n\n# ── Code Quality ──────────────────────────────────────────\n\n# Format all Go code\n[group('quality')]\nfmt:\n    golangci-lint fmt ./...\n\n# Check formatting without modifying (CI-safe)\n[group('quality')]\nfmt-check:\n    golangci-lint fmt --diff ./...\n\n# Run linter\n[group('quality')]\nlint:\n    golangci-lint run ./...\n\n# Run linter with auto-fix\n[group('quality')]\nlint-fix:\n    golangci-lint run --fix ./...\n\n# Run vulnerability check\n[group('quality')]\nvuln:\n    govulncheck ./...\n\n# ── Testing ───────────────────────────────────────────────\n\n# Run all tests with race detection\n[group('test')]\ntest *args=\"./...\":\n    gotestsum --format testname -- -race {{ args }}\n\n# Run tests with coverage\n[group('test')]\ntest-cov:\n    gotestsum --format testname -- -race -coverprofile=coverage.out -covermode=atomic ./...\n    go tool cover -func=coverage.out\n\n# Open coverage report in browser\n[group('test')]\ncoverage: test-cov\n    go tool cover -html=coverage.out\n\n# Run integration tests\n[group('test')]\ntest-integration:\n    gotestsum --format testname -- -race -tags=integration ./...\n\n# Watch tests during development\n[group('test')]\ntest-watch:\n    gotestsum --watch --watch-clear --format testname\n\n# Run benchmarks\n[group('test')]\nbench:\n    go test -bench=. -benchmem ./...\n\n# ── Build ─────────────────────────────────────────────────\n\n# Build the binary\n[group('build')]\nbuild:\n    go build -o {{ binary }} ./cmd/{{ binary }}\n\n# Build optimized release binary\n[group('build')]\nbuild-release:\n    CGO_ENABLED=0 go build -trimpath -ldflags=\"-s -w\" -o {{ binary }} ./cmd/{{ binary }}\n\n# ── Dependencies ──────────────────────────────────────────\n\n# Tidy and verify modules\n[group('deps')]\ntidy:\n    go mod tidy\n    go mod verify\n\n# Fail if go.mod/go.sum are untidy, without touching them (CI-safe)\n[group('deps')]\ntidy-check:\n    go mod tidy -diff\n\n# Run code generator"},{"language":"bash","snippet":"go install github.com/evilmartians/lefthook/v2@v2.1.17   # needs Go 1.26+\nlefthook install"},{"language":"yaml","snippet":"# lefthook.yml\nassert_lefthook_installed: true   # fail loudly instead of skipping every rule\n\npre-commit:\n  piped: true   # fail fast - stop at the first failing job\n  commands:\n    fmt:\n      glob: \"*.go\"\n      run: golangci-lint fmt {staged_files}\n      stage_fixed: true\n    lint:\n      glob: \"*.go\"\n      # Never pass a bare file list to `golangci-lint run`: a list spanning two\n      # directories is rejected outright, and one file of a multi-file package\n      # reports phantom `undefined:` typecheck errors. Lint the packages instead.\n      run: printf '%s\\n' {staged_files} | xargs -n1 dirname | sort -u | xargs golangci-lint run --fix\n      stage_fixed: true\n    mod-tidy:\n      glob: \"*.{go,mod,sum}\"\n      run: go mod tidy\n\npre-push:\n  commands:\n    test:\n      run: go test -race ./..."}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: go-dev\ndescription: Opinionated Go setup with golangci-lint v2, gofumpt, gotestsum, golang-migrate, and just. Use when starting a Go project, configuring lint, format, test, coverage or CI, writing a Justfile, wiring migrations, or leaving a Makefile workflow.\nmetadata:\n  version: \"0.6.0\"\n  categories: \"development\"\n  topics: \"go, golangci-lint, gofumpt, testing, just\"\n  upstream: \"go@1.27.1, golangci-lint@v2.14.0, gofumpt@v0.12.0, gotestsum@v1.13.0, golang-migrate@v4.20.1, just@1.58.0, lefthook@v2.1.17\"\n  openclaw:\n    homepage: https://github.com/tenequm/skills/tree/main/skills/go-dev\n    emoji: \"🐹\"\n    envVars:\n      - name: DATABASE_URL\n        required: false\n        description: Connection string used by the Justfile migration recipes (golang-migrate)\n---\n\n# Go Development Stack\n\nOpinionated, modern Go development setup. One tool per concern, zero overlap.\n\n## When to Use\n\n- Starting a new Go project from scratch\n- Adding linting, formatting, or testing infrastructure\n- Setting up CI/CD for a Go service or library\n- Creating a Justfile to replace a Makefile\n- Adding database migration tooling\n- Migrating from scattered gofmt/govet/staticcheck invocations to a unified setup\n\nNot for a fork that regularly merges from upstream: replacing its Makefile and reformatting the tree with gofumpt conflicts on every merge and buries real changes in a reformat diff. Keep upstream's gofmt and build tooling there, and only add checks.\n\n## The Stack\n\n| Tool | Version | Role | Replaces |\n|------|---------|------|----------|\n| **Go** | 1.27+ | Language, toolchain, `go mod`, `go fix` | - |\n| **golangci-lint** | v2.14+ | Meta-linter (100+ linters + formatters + `fmt` command) | gofmt, govet, staticcheck, errcheck run separately |\n| **gofumpt** | v0.12+ | Strict formatter (superset of gofmt, 19 default rules) | gofmt |\n| **gotestsum** | v1.13+ | Test runner with readable output, watch mode, JUnit XML | Raw `go test` |\n| **just** | 1.58+ | Task runner | Makefile |\n| **golang-migrate** | v4.20+ | DB migrations (CLI + library + `embed.FS`) | Manual SQL scripts |\n| **lefthook** | v2.1+ | Git hooks (single binary, parallel) | pre-commit (Python) |\n\n**Version floors are load-bearing.** golangci-lint \"supports Go versions lower or equal to the Go version used to compile it\" - a pin older than your Go toolchain fails outright. Go 1.27 support landed in golangci-lint v2.13.0, so `v2.13` is the floor for a Go 1.27 project; this skill pins v2.14.0, the first release that bundles gofumpt v0.12.0. Two more floors moved recently: gofumpt v0.12.0 \"is based on Go 1.27's gofmt, and requires Go 1.26 or later\", and lefthook's `go install` path now asks for Go 1.26+.\n\n## Quick Start: New Project\n\n```bash\n# 1. Create module\nmkdir myapp && cd myapp\ngo mod init github.com/yourorg/myapp\n\n# 2. Scaffold directories\nmkdir -p cmd/myapp internal migrations\n\n# 3. Install golangci-lint as a binary, not as a module tool (see note below)\ncurl -sSfL https://golangci-lint.run/install."},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn76gpsgjw5chv0xvzbzcb8cxn81x46r\",\n  \"slug\": \"go-dev\",\n  \"version\": \"0.6.0\",\n  \"publishedAt\": 1791288465096\n}"},{"path":"references/go-migrate-reference.md","content":"# golang-migrate Reference\n\nLatest: **v4.20.1** (2026-09-09). Built with Go 1.25/1.26. MIT license, 18K+ stars.\n\n**Pin v4.20.1, not v4.20.0.** A release-workflow bug meant v4.20.0 exists as a git tag but never reached Docker or the package registries - \"Due to a bug in the release workflow, GoReleaser failed and `v4.20.0` was not published to Docker or other package registries.\" v4.20.1 is that release redistributed, and carries no other changes.\n\nv4.20.0 is worth upgrading for regardless of the pin mechanics:\n\n- **S3 sources silently truncated at 1000 migrations** - \"fix(source/aws_s3): paginate ListObjects to load >1000 migrations\".\n- **Quadratic startup cost removed** - \"perf(source): build migrations index lazily to avoid quadratic startup\".\n- **Security, partial:** migrate's own code moved from `docker/docker` to the `moby/moby/api` and `moby/moby/client` modules, but v4.20.1's `go.mod` still lists `github.com/docker/docker v28.5.2+incompatible // indirect`, pulled in by `dhui/dktest`. Scanners keep flagging it - open issue [golang-migrate/migrate#1444](https://github.com/golang-migrate/migrate/issues/1444) \"Dependencies still trigger CVE-2026-41568\" (milestone v4.21.0).\n\n## Installation\n\n### CLI\n\n```bash\n# Homebrew (macOS)\nbrew install golang-migrate\n\n# Scoop (Windows)\nscoop install migrate\n\n# Pre-built binary\ncurl -L https://github.com/golang-migrate/migrate/releases/download/v4.20.1/migrate.linux-amd64.tar.gz | tar xvz\n\n# With Go (specify database driver via build tags)\ngo install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.20.1\n\n# Docker\ndocker run -v $(pwd)/migrations:/migrations --network host migrate/migrate \\\n    -path=/migrations/ -database \"postgres://localhost:5432/db\" up\n```\n\nMultiple drivers: `-tags 'postgres mysql sqlite3'`\n\n### Library\n\n```bash\ngo get github.com/golang-migrate/migrate/v4\n```\n\n## Migration File Naming\n\nFormat: `{version}_{title}.{direction}.sql`\n\n### Sequential (recommended for smaller teams)\n\n```bash\nmigrate create -ext sql -dir migrations -seq create_users_table\n```\n\nProduces:\n```\nmigrations/\n  000001_create_users_table.up.sql\n  000001_create_users_table.down.sql\n```\n\nControl zero-padding with `-digits N` (default: 6).\n\nTwo more `create` flags: `-format` takes \"a Go time format string\" for the version prefix, and `-tz` sets the timezone used to generate it.\n\n### Timestamp (better for larger teams)\n\n```bash\nmigrate create -ext sql -dir migrations create_users_table\n```\n\nProduces (default `-format` is `20060102150405`, a UTC timestamp):\n```\nmigrations/\n  20240405123456_create_users_table.up.sql\n  20240405123456_create_users_table.down.sql\n```\n\nEliminates version conflicts when multiple developers create migrations simultaneously.\n\nFor unix-epoch versions pass `-format unix` - \"If the string `\"unix\"` or `\"unixNano\"` is specified, then the seconds or nanoseconds since January 1, 1970 UTC respectively will be used.\" Combining `-seq` with any non-default `-format` fails with \"the seq and fo"},{"path":"references/go-testing-reference.md","content":"# Go Testing Reference\n\nCovers Go testing best practices, patterns, and tooling as of Go 1.27 (August 2026).\n\n## Table-Driven Tests\n\nThe idiomatic Go testing pattern. Use named struct slices with `t.Run()` for subtests:\n\n```go\nfunc TestUserValidation(t *testing.T) {\n    tests := []struct {\n        name    string\n        user    User\n        wantErr error\n    }{\n        {\n            name:    \"valid user\",\n            user:    User{Email: \"[email protected]\", Age: 25},\n            wantErr: nil,\n        },\n        {\n            name:    \"missing email\",\n            user:    User{Age: 25},\n            wantErr: ErrInvalidEmail,\n        },\n        {\n            name:    \"negative age\",\n            user:    User{Email: \"[email protected]\", Age: -1},\n            wantErr: ErrInvalidAge,\n        },\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            err := tt.user.Validate()\n            if !errors.Is(err, tt.wantErr) {\n                t.Errorf(\"Validate() error = %v, wantErr %v\", err, tt.wantErr)\n            }\n        })\n    }\n}\n```\n\n**Note:** Since Go 1.22, the loop variable capture bug is fixed. `tt := tt` inside the loop is no longer needed, even with `t.Parallel()`.\n\n## Parallel Tests\n\n```go\nfunc TestParallel(t *testing.T) {\n    tests := []struct {\n        name  string\n        input int\n        want  int\n    }{\n        {\"double 1\", 1, 2},\n        {\"double 5\", 5, 10},\n    }\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            t.Parallel() // Safe without tt := tt in Go 1.22+\n            got := Double(tt.input)\n            if got != tt.want {\n                t.Errorf(\"got %d, want %d\", got, tt.want)\n            }\n        })\n    }\n}\n```\n\nDefault parallelism = `GOMAXPROCS`. Override with `go test -parallel N`.\n\n## Testing Helpers (Go 1.14-1.27)\n\n### t.Helper()\n\nMark functions as test helpers so failures report the caller's line:\n\n```go\nfunc assertNoError(t testing.TB, err error) {\n    t.Helper()\n    if err != nil {\n        t.Fatalf(\"unexpected error: %v\", err)\n    }\n}\n```\n\nUse `testing.TB` as the parameter type so helpers work in both tests and benchmarks.\n\n### t.Cleanup(func()) - Go 1.14\n\nRegister cleanup that runs after the test and all subtests complete. LIFO order:\n\n```go\nfunc newTestDB(t *testing.T) *DB {\n    t.Helper()\n    db := openDB()\n    t.Cleanup(func() { db.Close() })\n    return db\n}\n```\n\n### t.TempDir() - Go 1.15\n\nAuto-cleaned temporary directory:\n\n```go\nfunc TestWriteFile(t *testing.T) {\n    dir := t.TempDir() // Removed automatically after test\n    path := filepath.Join(dir, \"output.txt\")\n    err := os.WriteFile(path, []byte(\"hello\"), 0o644)\n    require.NoError(t, err)\n}\n```\n\n### t.Setenv(key, value) - Go 1.17\n\nSet env var for test duration, restored on cleanup:\n\n```go\nfunc TestConfig(t *testing.T) {\n    t.Setenv(\"DATABASE_URL\", \"postgres://test@localhost/testdb\")\n    cfg := LoadConfig()\n    assert.Equal(t, \"postgres://test@localhost/testdb\", cfg.DatabaseURL)\n}\n```\n\n"},{"path":"references/gofumpt-reference.md","content":"# gofumpt Reference\n\nLatest: **v0.12.0** (2026-09-07). Based on Go 1.27's gofmt - \"This release is based on Go 1.27's gofmt, and requires Go 1.26 or later.\"\n\ngofumpt is a **strict superset of gofmt** - any code formatted by gofumpt produces zero changes when processed by gofmt. It adds 19 opinionated formatting rules on top, plus 3 opt-in extra rules.\n\n**Upgrading to v0.12.0 reformats imports.** Four fixes change output on real code, so expect a one-time diff: a std import carrying a comment is \"no longer moved into the top import group, as the comment stayed behind and ended up detached at the bottom of the group\"; moving a std import up \"no longer leaves an empty line where it used to be\"; a copyright header or package doc \"no longer makes gofumpt treat a single-line first declaration as multi-line\"; and an assignment whose right-hand side is split by a comment \"is now left alone\". Go 1.27's gofmt adds a fifth source of churn: a column-alignment fix means \"running gofmt from Go 1.27 on previously formatted code may produce minor whitespace changes\", and v0.12.0 inherits it from `go/printer`. Land the reformat as its own commit.\n\n**golangci-lint lags gofumpt.** golangci-lint v2.14.0 bundles `mvdan.cc/gofumpt v0.12.0`, so today `golangci-lint fmt` and a standalone v0.12.0 binary agree (v2.13.2 still vendored v0.11.0 and disagreed on exactly the cases above). The lag recurs whenever gofumpt releases first: master already carries 33 unreleased commits (2026-09-22/23), 16 of them output-changing `format:` fixes such as \"format: don't join imports whose comments would be left behind\". Do not use one as fixer and the other as gate.\n\n## Installation\n\n```bash\n# From source (recommended)\ngo install mvdan.cc/gofumpt@v0.12.0\n\n# Pre-built binaries from GitHub Releases\n# Available for darwin/linux/windows on amd64/arm64\n\n# Via gopls (no separate binary needed for editor use)\n# Configure your editor to tell gopls to use gofumpt formatting\n```\n\n## CLI Usage\n\n```bash\ngofumpt -w .                  # Format all Go files recursively, in-place\ngofumpt -l .                  # List files that differ from gofumpt style\ngofumpt -d main.go            # Show diff without modifying (non-zero exit if diff exists)\ngofumpt -w main.go            # Format single file in-place\ngofumpt -extra=group_params,clothe_returns .  # Explicit extra rules (the \"=\" is required)\ngofumpt -extra .              # All three extra rules, balance_calls included\ngofumpt -lang=go1.27 .        # Specify language version\ngofumpt -modpath=github.com/org/repo .  # Specify module path\ngofumpt -version              # Print version\ncat main.go | gofumpt         # Format from stdin\n```\n\n**Flags:**\n- `-w` - write result to file (instead of stdout)\n- `-l` - list files that differ\n- `-d` - display diff (non-zero exit if any diff, since v0.8.0)\n- `-extra` - enable extra rules. **Changed in v0.10.0:** \"The `-extra` flag now accepts a comma-separated list of rule names to enable individual extra rules, rather th"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1524,"uniquenessScore":43,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T03:58:51.269Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T08:44:05.643Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}