# Go - Lint and Vulnerability Tooling
Most lint suppression failures are placement or tool-behavior questions, not code questions. Lint directives work only in exact positions, `gofmt` undoes hand-padded comment spacing, and `unparam` hides a call-site threshold. `govulncheck` blocks CI only on reachable vulnerabilities, but its output is only as good as the vulnerability database.
## Key points
- Put `//nolint:<linter>` on the line directly above the function declaration. Same-line comments and doc-comment position do not suppress function-level checks. In golangci-lint v2, `nolintlint` then flags the directive as unused while the linter still fires.
- One allowlist entry in `.golangci.yml` covers deliberately-returned interfaces, so you avoid per-function annotations.
- A standalone `//nolint` covers only the value's own line and the line above it in a multi-line call. Later lines still get reported.
- Do not pad directive comments with extra spaces to align them. `gofmt` re-aligns comments within a struct field group to its own rules. The manual padding is undone and the format check fails.
- `gochecknoglobals` fires on package-level `var` in test files. Write test helpers as functions, not vars, when the linter runs without a `testpackage` exception.
- Reduce cyclomatic complexity (`cyclop`) by extracting validation into narrow helpers instead of one large validate function. Each branch then becomes testable on its own.
- Run `golangci-lint` and its `--fix` from each module directory in a multi-`go.mod` repository. Root-module runs miss sub-module violations. Run `goimports` after `--fix`, because some fixes leave a stale import behind.
- `unparam` reports a function only when it has at least 4 call sites. The fourth call site flips CI lint from green to failing without any wrong code. Count call sites before you add one.
- Run a custom analyzer with `go vet -vettool=/path/to/analyzer ./...`. You get standard package-pattern handling and diagnostics for free.
- Treat unit suffixes after a digit as initialisms in identifiers: `Ephemeral1H`, `Ephemeral5M`, `Buffer64K`, never `1h` or `5m`.
- Analyzer edge case: a `*` width or precision operand advances the format index but not the operand counter. `%*v` then misattributes the width operand.
- `govulncheck` works at the call-graph level. It flags only vulnerabilities in code paths the binary actually reaches. This produces far fewer false positives than module-level scanning such as Dependabot's.
- Rebuild `govulncheck` with the current Go toolchain before you run a CI-equivalent scan: `go install golang.org/x/vuln/cmd/govulncheck@latest`. A binary built by an older toolchain diverges from what CI sees.
- Make the blocking CI job fail only on reachable, symbol-level vulnerabilities. Keep module-level findings informational.
- An unmaintained module that an OSV entry marks fully affected, with no fixed version, can never be cleared. A version bump only pins the last tag. The finding stays open forever.
- A vulnerability database can mark a fixed release as affected. GO-2026-6872 and GO-2026-6875 credit the `echo/v4` fix only to v5, while v4.16.3 does contain the fix and the GHSA range ends at 4.16.2. `govulncheck` then flags more than 200 symbols even on the patched release. Verify the advisory's affected range and the upstream fix before you bump. The failure is a database defect, not a code problem (golang/vulndb#6924).
## How to apply
1. Place `//nolint` directives on the line above the target declaration.
2. In each module directory, run `golangci-lint run --fix`, then run `goimports` on the changed files.
3. Rebuild the scanner with the current toolchain, then scan: `go install golang.org/x/vuln/cmd/govulncheck@latest` and `govulncheck ./...`.
4. Run custom analyzers with `go vet -vettool=/path/to/analyzer ./...`.
5. Before a dependency bump for a vulnerability, check the advisory's affected range against the upstream fix. A finding that survives a bump to the latest tag is a database or maintenance signal, not a code problem.
6. Count the call sites of a function before you add a new one, when `unparam` is active.
## Observations
- [tooling] Directives placed in doc-comment position stop working in golangci-lint v2. `nolintlint` flags them unused while the linter still fires. #golangci-lint (source: agent sessions, 2026-07)
- [tooling] `gofmt` re-aligns struct-field comment groups and undoes manual padding, which breaks format checks. #gofmt (source: agent sessions, 2026-02)
- [tooling] `unparam` hides a call-site threshold of 4. The fourth call site flips CI lint from green to failing. #unparam (source: agent sessions, 2026-10)
- [tooling] Root-module lint runs miss sub-module violations in multi-`go.mod` repositories. #golangci-lint (source: agent sessions, 2026-07)
- [security] `govulncheck` flags only reachable vulnerabilities. This gives precise CI blocking with far fewer false positives than module-level scanners. #govulncheck (source: agent sessions, 2026-03)
- [security] An unmaintained module with no fixed version can never be cleared by a dependency bump. #vulnerability-management (source: agent sessions, 2026-09)
- [security] A vulnerability database entry can mark a patched release as affected. Verify the advisory range before you trust a failing scan. #vulnerability-management (source: agent sessions, 2026-11)
## Relations
- relates_to [[resources/documentation/Go/Go - Modules, Workspaces, and GOPROXY]] (per-module lint runs and dependency bumps in multi-module repositories)
- relates_to [[resources/documentation/Go/Go - Toolchain Pinning and the Build Cache]] (rebuilding govulncheck with the current toolchain)
- relates_to [[resources/documentation/Renovate and Dependabot - Automation Semantics]] (module-level scanning versus call-graph analysis)
- relates_to [[resources/documentation/Go/Go - Correctness Idioms]] (identifier naming and validation-helper structure)
## Sources
- Personal agent-session notes, February 2026 to November 2026