Skip to content

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*"]
revive -config revive.dev.toml ./...   # local
revive -config revive.toml ./...        # CI
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

Beforerevive 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

Beforerevive ./... lints vendor/, internal/mock/, and 50 .pb.go files generated by protoc.

After:

revive -exclude vendor/... \
       -exclude ./internal/mock/... \
       -exclude ./api/gen/... \
       ./...
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:

confidence = 0.95   # was 0.8
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 revive on changed packages only in pre-commit/PR loops.
  • Cache GOCACHE and ~/go/pkg/mod in CI; pin the revive version.
  • Keep a smaller dev config; the full policy runs on merge.
  • Disable continuous revive in the editor; let gopls/staticcheck cover 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 confidence as a temporary gate while clearing a rule's backlog, not a permanent silencer.