Go Code Quality: Opinionated by Design
Go teams rarely argue about tabs vs spaces.
Search across all documentation pages
Go teams rarely argue about tabs vs spaces.
The language and toolchain ship with defaults that encode decades of production experience, then leave room for teams to add stricter gates as codebases mature.
gofmt), static correctness (go vet), and tests as non-negotiable baselines; everything else (staticcheck, gosec, custom analyzers) extends that stack.Go's designers chose boring consistency over configurability for style.
gofmt rewrites source to one canonical layout.
There is no project-level style file to fight over in review.
goimports extends formatting by adding, removing, and grouping imports.
Together they answer: "Does this file look like Go?"
go vet is the next layer.
It runs analyzers maintained with the Go release that flag suspicious constructs: printf verbs that do not match arguments, unreachable code, misuse of sync.Mutex, struct tags that JSON cannot parse, and more.
Vet is not a style checker.
It targets mistakes the compiler allows but humans regret.
The third baseline is tests.
go test is the universal runner; -race adds a data-race detector; coverage and benchmarks live in the same tool.
Quality culture in Go assumes green tests before merge.
Teams that need more reach for the analyzer ecosystem built on go/analysis.
staticcheck finds dead code and API misuse.
gosec flags risky crypto and injection patterns.
golangci-lint orchestrates dozens of linters with one YAML config.
The typical maturity ladder:
Day 1 gofmt + goimports (editor + CI diff check)
Week 1 go vet ./... + go test ./...
Month 1 golangci-lint preset + govulncheck in CI
Quarter 1 Custom analyzers for domain APIs + review rubricQuality tools interact at three speeds: editor, local hook, and CI.
gopls runs a subset of analyzers while you type.
Pre-commit hooks run formatters and fast checks before git push.
CI runs the authoritative bundle on a clean checkout with pinned Go and linter versions.
Formatting checks usually compare gofmt -l output to an empty list or use gofmt -w in a fix-up job.
Import hygiene uses goimports -local example.com/mycorp to keep internal modules grouped separately from third-party paths.
Vet and golangci-lint share analyzer DNA.
Many go vet checks appear inside golangci-lint as the govet linter.
Running both is redundant but harmless; scripts often keep go vet for clarity in minimal pipelines.
| Layer | Tool | What it enforces | Typical failure mode |
|---|---|---|---|
| Format | gofmt, goimports | Layout and imports | CI fails on unformatted diff |
| Correctness | go vet, staticcheck | Suspicious APIs, dead branches | Analyzer diagnostic |
| Security | gosec, govulncheck | Risky patterns, known CVEs | Block merge on high severity |
| Behavior | go test, -race | Logic and concurrency | Test or race failure |
Monorepos add path-scoped configs.
A root .golangci.yml can set defaults; per-module overrides disable linters for generated protobuf or vendor/.
go.work workspaces need lint commands that respect each module's go.mod boundary.
Incremental adoption beats big-bang strictness.
Start with gofmt, go vet, and tests.
Add golangci-lint with the standard or fast preset.
Enable staticcheck, errcheck, and unused before niche linters.
For legacy code, use //nolint with a ticket reference or linter exclude rules for directories until refactors land.
Generated code (*.pb.go, mock_*.go) should be excluded from style linters but still compile and test.
Library vs service policy differs.
Published modules face consumers on older Go versions; pin go directive and run vet on all supported versions in CI matrix.
Internal services can move faster on toolchain bumps.
Human review still covers architecture, naming taste, and operational concerns linters cannot see.
Encode objective rules in analyzers; document subjective judgment in Code Review Standards for Go Teams.
| Strategy | Strength | Weakness | Best fit |
|---|---|---|---|
| Minimal (gofmt + vet + test) | Fast onboarding, low noise | Misses security and deep static bugs | Small tools, early startups |
| Standard golangci-lint | Broad coverage, one config | Tuning time, occasional false positives | Most production services |
| Custom go/analysis rules | Domain-specific guarantees | Maintenance cost | Platforms with public SDKs |
| Format-only in OSS | Low contributor friction | Inconsistent deeper quality | Casual open-source libs |
//nolint spam; grow the set as the codebase absorbs each rule.One format maximizes readability across repos and minimizes bike-shedding.
Tools and humans recognize Go source instantly.
gofmt -s also applies simplifications (e.g., slice patterns).
Many teams run plain gofmt; simplifications are optional but common in hooks.
goimports runs gofmt and manages import blocks.
Use goimports in editors; CI can check either tool as long as the team agrees.
New Go minors often add vet analyzers.
Treat release notes tooling sections as CI-affecting, not optional reading.
After gofmt, go vet, and go test ./... are green in CI.
Add when review repeatedly catches the same staticcheck-class issues.
Libraries benefit from stricter API checks (revive, ireturn).
Services add security and operational linters (gosec, bodyclose for HTTP).
Pin golangci-lint version in CI.
Document the same version for local make lint.
gopls gives early signal but CI remains authoritative.
Some linters assume desktop GOOS.
Scope lint to packages that build for each target or exclude tinygo build tags in config.
A documented make check and fast pre-commit hook onboard faster than oral tradition.
Opinionated defaults reduce "how we do things here" lectures.
Effective Go is human guidance.
Automate what is objective (format, vet, common staticcheck rules); keep the rest in review standards.
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).
Reviewed by Chris St. John·Last updated Jul 16, 2026