golangci-lint

LanguageGo
Kindtool
Readiness🟡
Detector conformance🟡
Checked2026-09-08, version v2.13.2

golangci-lint is the standard aggregator for Go: one binary that runs staticcheck, go vet, errcheck, unused and around a hundred other linters over a shared parse, configured from a single .golangci.yml. In a pipeline it is usually the only lint entry point a Go project has, so it is the natural place for a bespoke defence, and it does support one, at the cost of building a custom binary.

How it is conformant

Bespoke rules are supported through the module plugin system: a project writes a go/analysis analyzer, lists it in .custom-gcl.yml, builds a custom binary with golangci-lint custom, and enables it under linters.settings.custom with type: module (4.1). The go/analysis framework’s analysistest package runs a single analyzer against testdata with // want comments as the red proof (analysistest, 4.2). The linter name is chosen once by its author and is what golangci-lint prints in parentheses after each finding, in text and in every reporter format (4.3, for the linter name). golangci-lint run file1.go runs locally on one file, and --default=none --enable <name> runs one linter (quick start), meeting 5.1, 5.2 and 5.4, with every finding in the command’s own output (5.3). golangci-lint help linters describes every linter from the binary, and a custom analyzer’s Doc string ships with it (6.1, 6.2, 6.3 at linter level). Clause 7.1 holds because the inline route is detectable by a bundled rule: the nolintlint linter reports //nolint directives, and its require-explanation and require-specific settings (reference configuration) reject one without a reason, which is exactly the shape of 7.2. The issues.new-from-rev and new-from-merge-base baselines are configuration the project can simply not set.

How it is not conformant

Clause 4.3 is partial: the identifier printed is the linter name, so a plugin carrying several rules prints one name for all of them unless it registers each as its own custom linter, and findings from wrapped tools carry that tool’s own code, such as SA1019, which is stable but which golangci-lint does not itself resolve. That leaves 6.1 to 6.3 partial as well: help linters resolves a linter name, but a wrapped tool’s rule code resolves only on that tool’s website, not from the golangci-lint binary. nolintlint is off unless enabled, so 7.2 is met by configuration rather than by default. There is no rule over the rules under 4.4, and no release gate over bundled documentation is documented (6.4, not verified). Readiness is 🟡 rather than 🟢 because the bespoke route requires a custom build rather than a configuration entry, and because of the one-name-per-plugin-linter limit.

Clause by clause

Document Clause Result Evidence
Detector 4.1 Yes Module plugins via a custom binary
Detector 4.2 Yes analysistest
Detector 4.3 Partial Linter name printed with every finding; one name per plugin linter; wrapped tool codes pass through unresolved
Detector 4.4 No None
Detector 5.1 Yes golangci-lint run locally (quick start)
Detector 5.2 Yes golangci-lint run file1.go
Detector 5.3 Yes Findings printed with linter name
Detector 5.4 Yes --default=none --enable <name>
Detector 6.1 Partial help linters resolves a linter name, not a wrapped tool’s code
Detector 6.2 Partial Linter descriptions in the binary; wrapped rule docs on the website
Detector 6.3 Partial Custom analyzer Doc ships with the analyzer; wrapped tool docs do not
Detector 6.4 Not verified No release gate documented
Detector 7.1 Yes nolintlint detects //nolint (false positives); baselines are opt-in
Detector 7.2 Yes nolintlint require-explanation (reference config), off by default

Notes for a practitioner

Write the class as a go/analysis analyzer with a chosen Name, prove it with analysistest against a testdata fixture, and ship it through a module plugin so the project’s usual golangci-lint run enforces it. Enable nolintlint with require-explanation and require-specific from day one, and do not set new-from-rev, so that every suppression is visible and reasoned even though it sits in the code rather than a record.