revive — Optimization¶
revive's wall time is dominated by package loading (calling go list and parsing source) and the per-file rule loop. These exercises reduce the cost in the inner dev loop and in CI. Numbers are illustrative; measure on your machine with time.
Exercise 1: Lint only changed packages¶
Before — every save runs revive ./... across the whole monorepo: several seconds, mostly loading.
After:
CHANGED=$(git diff --name-only origin/main...HEAD | grep '\.go$' \
| xargs -n1 dirname | sort -u | sed 's|^|./|;s|$|/...|')
[ -z "$CHANGED" ] && exit 0
revive -config revive.toml $CHANGED
| Metric | revive ./... | changed-only |
|---|---|---|
| Loaded packages | all (~1,200) | ~6 |
| Wall time on big repo | ~8s | ~0.5s |
For pre-commit hooks and PR feedback loops this is the single biggest win.
Exercise 2: Cache module and build caches in CI¶
Before — each CI run downloads modules and re-loads packages from scratch.
After — persist Go caches across jobs:
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-${{ hashFiles('**/go.sum') }}
| Metric | Cold caches | Warm caches |
|---|---|---|
go install revive@v1.5.1 | ~30s (download + build) | ~1s (binary cached) |
revive ./... | ~12s | ~5s |
Pin the revive version in the cache key, otherwise upgrades silently reuse stale binaries.
Exercise 3: Restrict the rule set in the inner loop¶
Before — the team config enables 18 rules; revive runs all of them on every save.
After — keep a revive.toml for CI and a smaller revive.dev.toml for local development that disables the cheap-but-noisy stylistic rules:
# revive.dev.toml — fast subset for local edits
[rule.error-return]
[rule.error-strings]
[rule.unhandled-error]
arguments = ["fmt.Print*"]
| Metric | full 18 rules | dev subset (3 rules) |
|---|---|---|
| Wall time on touched package | ~1.2s | ~0.4s |
The trade-off is intentional: IDE/editor integration (gopls + golangci-lint) covers the slower stylistic rules continuously, while the local revive run focuses on the few rules that block merges.
Exercise 4: Stop running revive in the IDE¶
Before — revive is wired into the editor's save hook, runs on every keystroke save (debounced), and re-loads packages each time.
After — let gopls and staticcheck handle continuous in-editor feedback (they have warm in-memory state). Run revive only on:
- pre-commit
- pre-push
- CI
| Metric | revive-on-save | revive-on-merge |
|---|---|---|
| Editor latency per save | +600ms | 0 |
| Lint runs per PR | dozens | 1–2 |
revive is built to be fast for one-off invocations, not for an in-process continuous loop. Use the right tool for the loop you are in.
Exercise 5: ndjson for fast CI parsing¶
Before — CI pipes the default formatter through grep | wc -l to count findings, occasionally breaking on wording changes.
After:
revive -config revive.toml -formatter ndjson ./... > findings.ndjson
ERRORS=$(jq -r 'select(.Severity == "error")' findings.ndjson | wc -l)
WARNS=$(jq -r 'select(.Severity == "warning")' findings.ndjson | wc -l)
echo "::warning::revive: $WARNS warnings, $ERRORS errors"
| Metric | grep on text | jq on ndjson |
|---|---|---|
| Robust across versions | no | yes |
| Per-finding fields available | only message | full position, rule, severity, category |
| Parse cost on 5,000 findings | ~80ms | ~50ms |
ndjson is line-oriented so it streams; you can pipe directly into jq without buffering the whole document.
Exercise 6: Exclude generated and vendored code¶
Before — revive ./... lints vendor/, internal/mock/, and 50 .pb.go files generated by protoc.
After:
| Metric | linting everything | excluding generated/vendored |
|---|---|---|
| Files loaded | 4,200 | 1,100 |
| Wall time | ~12s | ~3s |
| Useful findings | 47 (after filtering noise) | 47 |
Also verify every generator emits the canonical header so ignoreGeneratedHeader = false skips them automatically — -exclude is the belt to that suspender.
Exercise 7: Raise confidence to suppress marginal findings¶
Before — a recently enabled rule is producing 150 borderline findings the team will not act on; people start ignoring revive output entirely.
After — raise the threshold temporarily while the backlog is worked down:
| Metric | confidence 0.8 | confidence 0.95 |
|---|---|---|
| Findings | 412 | 178 |
| Signal-to-noise | low | high enough to act on |
Use this as a gate, not a hiding spot: schedule the work to lower it back to 0.8 once the high-confidence backlog is cleared. Leaving it at 0.95 forever just means you trust the linter less than it deserves.
Measurement checklist¶
- Run
reviveon changed packages only in pre-commit/PR loops. - Cache
GOCACHEand~/go/pkg/modin CI; pin thereviveversion. - Keep a smaller dev config; the full policy runs on merge.
- Disable continuous
revivein the editor; letgopls/staticcheckcover it. - Parse CI output with
ndjson+jq, not by grepping the default text format. - Exclude generated and vendored paths; verify the generated-file header detection works.
- Use
confidenceas a temporary gate while clearing a rule's backlog, not a permanent silencer.