Checks a Go codebase against the Go Style Guide and reports every finding with its rule ID, linked to the rule:
internal/invoice/service.go:42:10: MUST GO-08.3: say what this function was doing, without "failed to" or "error": "failed to save invoice: %w" [chopper]
internal/httpapi/server.go:17:2: MUST NOT GO-14.14: use one http.Client per dependency with a Timeout, not http.Get [chopper]
internal/postgres/store.go:88:3: MUST GO-15.8: rows.Err must be checked [golangci-lint:rowserrcheck]
It runs two kinds of check:
- golangci-lint v2 with the guide's
.golangci.yml(Appendix A): the linters, plus the gofumpt and goimports formatters (chapter 5). It runs once per Go module (go.mod) under the paths, and findings are mapped to the rule they enforce; any other golangci-lint finding is reported as GO-05.7, since CI fails on all of them. A module that declares golangci-lint with thetooldirective ingo.mod(GO-02.3) is linted with its owngo tool golangci-lint; otherwisegolangci-lintonPATHis used. With neither, the kit checks formatting withgofmtand says so. - The guide's own checks for rules golangci-lint does not express: the
cmd/andinternal/layout, package and file names, import groups, receivers,ctxfirst anderrorlast, error strings anderrors.AsType, panics andos.Exitoutsidemain, logging withslog,os.Getenvoutside config, HTTP servers and clients, SQL built withfmt.Sprintf, test conventions, doc comments, TODO owners, andgo.mod,go.sum, Taskfile and README checks.checks.jsonlists them all.
The repository's own .golangci.yml is used when it has one (the nearest one above each module); otherwise the kit's copy (the guide's appendix, verbatim) is, with example.com/project in goimports.local-prefixes replaced by the module path. go vet runs inside golangci-lint (govet); govulncheck needs the network and stays in your task lint.
Run it
Needs Python 3.9+, and Go with golangci-lint v2 for the tool checks. The guide's template already declares golangci-lint in go.mod, so go tool builds it on first use; elsewhere install it from golangci-lint.run.
python3 chopper_lint.py # the current folder
python3 chopper_lint.py internal cmd # some folders
python3 chopper_lint.py --build-tags integration # lint integration-tagged files too
python3 chopper_lint.py --strict # fail on SHOULD and AVOID too
python3 chopper_lint.py --no-golangci # the guide's own checks only
python3 chopper_lint.py --list-checks # what it checks
Folders named vendor, testdata, third_party, node_modules, bin and dist are skipped, as are hidden folders. Generated files (// Code generated ... DO NOT EDIT.) are only checked for their name. Run go mod download (and go generate) first, so golangci-lint can type-check every package; the kit notes any package it could not load.
Exit status: 1 when a MUST or MUST NOT rule is broken (any finding with --strict), else 0.
In GitHub Actions
- uses: actions/checkout@v4
- uses: carson-hopper/chopper.dev/content/styleguides/go/lint@main
with:
paths: .
# go-version-file: services/api/go.mod # where the Go version is read from (default go.mod)
# build-tags: integration
The action installs Go with actions/setup-go (from go-version-file, or go-version) and golangci-lint v2.15.0 with golangci/golangci-lint-action (install-only), both pinned to a commit; set golangci-lint-version to change it. Modules that declare golangci-lint with go tool use their own pinned version instead. Findings appear as annotations on the pull request. Pin @main to a commit SHA for reproducible runs.
Silencing a finding
On the same line or the line above, with a reason:
os.Exit(code) // chopper-lint: allow GO-08.14 (the CLI's run helper owns the exit code)
For a whole file: // chopper-lint: allow-file GO-08.14. It works in go.mod too, for the module checks. An allow comment also silences a golangci-lint finding mapped to that rule; for the linters themselves, use //nolint:<linter> // <reason> (GO-05.8).