> **go-linters** — skill 18 of 54 in [github/gh-aw](https://skillsdocs.com/github/gh-aw).
>
> Book (all skills, one file): https://skillsdocs.com/github/gh-aw.md
> Machine manifest: https://skillsdocs.com/github/gh-aw/.well-known/agent-skills/index.json
> Install the book: `npx skills add github/gh-aw`
> Upstream: https://github.com/github/gh-aw/blob/main/.github/skills/go-linters/SKILL.md @ `main`
> Raw bytes, no header: https://raw.githubusercontent.com/github/gh-aw/main/.github/skills/go-linters/SKILL.md
> Base for relative paths: https://raw.githubusercontent.com/github/gh-aw/main/.github/skills/go-linters/
> Licence: MIT — https://spdx.org/licenses/MIT.html
>
> Content © its authors, served unmodified. Takedown: https://github.com/DreambaseAI/skillsdocs/issues/new?labels=takedown&title=Takedown+request

<!-- Verbatim upstream SKILL.md follows, YAML frontmatter included. -->

---
name: go-linters
description: Add and validate custom Go analysis linters in gh-aw.
---

# Go Linters

Use this guide when adding a new custom Go analysis linter in this repository.

For PR-driven linter generation (derive a rule from a specific pull request pattern), use `.github/skills/pr-to-go-linter/SKILL.md`.

## Where to add a new linter

1. Create a new package under `pkg/linters/<linter-name>/`.
2. Define an analyzer in that package (exported as `Analyzer`).
3. Add tests in the same package using `analysistest` with fixtures under `testdata/src/...`.
4. Register the analyzer in `cmd/linters/main.go` so it runs via the multichecker binary.

## Build and test linters

- Test only your linter package:
  - `go test ./pkg/linters/<linter-name>/...`
- Build the custom linter runner:
  - `go build ./cmd/linters`
- Run all custom linters across the repo:
  - `make golint-custom`

`make golint-custom` builds `cmd/linters` and runs it against `./cmd/...` and `./pkg/...`.

## Coverage-aware perf gating

For linters that flag micro-optimizations (allocation/perf rules), only apply them on lines that
tests actually exercise — "hot paths" — rather than on dead or rarely-executed code where the
optimization brings no measurable benefit. Use the shared `pkg/linters/internal/coverage` package:

1. In your analyzer file, register a `-hot-threshold` flag in `init()` (not as a var initializer,
   to avoid an `Analyzer`/`run`/flag initialization cycle):

   ```go
   var hotThreshold *int

   func init() {
       hotThreshold = coverage.RegisterHotThresholdFlag(Analyzer)
   }
   ```

2. Immediately before reporting a diagnostic, gate it with `coverage.ShouldApply`:

   ```go
   if !coverage.ShouldApply(pass, node.Pos(), *hotThreshold) {
       return
   }
   ```

`coverage.ShouldApply` is permissive by default: when no coverage profile is loaded via the
`GH_AW_LINT_COVERAGE_PROFILE` environment variable, or when `hot-threshold` is `0`, it always
returns `true`, preserving pre-coverage-aware behavior. Only wire this into linters whose fix has
a genuine performance rationale (extra allocations, O(n²) behavior, etc.) — purely
readability/style linters should not be coverage-gated.

### Generating the coverage profile

```bash
go test -covermode=count -coverprofile=/tmp/coverage.out ./...
export GH_AW_LINT_COVERAGE_PROFILE=/tmp/coverage.out
make golint-custom
```

This profile is read once per linter-runner process. To lint only a specific subtree, scope
the `go test` and `golint-custom` commands to the same package path.
