Linting & Code Quality Best Practices
Balancing strictness, velocity, and incremental adoption.
These rules distill the linting-quality section: automate the boring checks, grow linter strictness with the codebase, and keep CI authoritative.
How to Use This List
- Apply when defining platform standards or auditing a service before production launch
- Start new repos at Tier A; promote to Tier B after first release
- Pair with
make checkin CONTRIBUTING and required CI status checks - Revisit quarterly when Go minors add vet checks or golangci-lint defaults shift
- Use Code Review Standards for Go Teams for human judgment items
A - Non-negotiable baselines
- Enforce gofmt or goimports in CI. Unformatted code never merges; provide
make fmtfor one-command fixes. - Run
go vet ./...on every PR. Vet targets likely bugs the compiler misses. - Require
go test ./...green. Quality includes behavior; lint cannot replace tests. - Pin Go version in CI to match
go.mod. Toolchain drift causes false green or false red builds. - Document local setup in README. Install steps for goimports, golangci-lint, and
make check.
B - CI merge gates
- Add
go test -racefor services with concurrency. Race failures block merge, not nightly email. - Run golangci-lint with explicit enable list. Avoid surprise new default linters by pinning action version.
- Run govulncheck on
./...after module changes. Triage reachable CVEs with upgrade PRs, not silent ignores. - Verify
go mod tidyproduces no diff. Keepsgo.sumhonest in review. - Exclude generated
*.pb.goand mocks from style linters. Still compile and test generated code.
C - Local developer experience
- Keep pre-commit hooks under ~60 seconds. Heavy lint belongs in pre-push or CI, not every WIP commit.
- Match hook
-localprefixes and tool versions to CI. Eliminates "passes locally, fails in pipeline" friction. - Enable gopls format on save aligned with goimports policy. Editors catch issues before hooks run.
- Provide
make lintandmake test-racetargets. One vocabulary for humans and agents. - Restrict
--no-verifyto emergencies with follow-up. Document in team agreement.
D - Incremental strictness
- Enable staticcheck after vet is clean for one sprint. SA codes find dead APIs and misuse.
- Add gosec when handling HTTP, SQL, or crypto. Security linters complement govulncheck module scans.
- Use
//nolintwith ticket ID and narrow scope. Never blanket-disable linters on packages without owners. - Sunset exclude-rules with dates. Legacy directory exclusions need refactor deadlines.
- Bump golangci-lint on schedule, not ad hoc. Read changelog; fix new findings in dedicated PR.
E - Review and culture
- Treat lint as floor, review as ceiling. Reviewers check context, errors, and operability per cheatsheet.
- Do not debate formatting in comments. Point to
make fmtand move on. - Celebrate shrinking linter suppressions. Trend
//nolintcount down each quarter. - Share new vet analyzer failures after Go upgrades. Post summary in #eng when release notes add checks.
- Align library and service policies explicitly. Published modules may stay one minor stricter on API linters.
FAQs
What is the minimum viable quality bar?
gofmt, go vet, and go test ./... in CI.
Add golangci-lint and race when the team has more than a handful of packages.
How strict should golangci-lint be on day one?
Start with govet, errcheck, gosimple, ineffassign, unused, staticcheck.
Add revive and gosec after noise baseline is zero.
Should OSS projects use the same gates as internal services?
OSS can stay at format + vet + test for contributor friction.
Run govulncheck and race in maintainer CI before tags.
How do monorepos avoid 30-minute lint jobs?
Shard per module in matrix, cache modules, use --new-from-rev on PRs.
Keep nightly full-repo scan.
When is custom lint worth it?
When staticcheck cannot express stable org API rules.
Maintain custom analyzers like production code.
What about tinygo or wasm builds?
Scope lint to packages that build for each target.
Document excluded build tags in .golangci.yml.
How do quality gates interact with go fix modernizers?
Run go fix on upgrade branches; commit separately from feature work.
Vet may fail until modernizers apply.
Should quality block hotfixes?
Hotfixes still need tests and vet.
Document expedited review, not skipped race/vuln on production paths.
Related
- Go Code Quality: Opinionated by Design - section explainer
- Code Quality Basics - hands-on intro
- golangci-lint Configuration - config depth
- CI Quality Gates: Lint, Test, Race, Vuln - pipeline template
- staticcheck & golangci-lint in CI - analyzer CI patterns
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).