staticcheck & golangci-lint in CI
staticcheck is a high-signal analyzer for bugs and API misuse.
Search across all documentation pages
staticcheck is a high-signal analyzer for bugs and API misuse.
golangci-lint runs staticcheck and dozens of other checks behind one config file, making it the usual CI entry point for Go lint policy.
Local editors give fast feedback; CI enforces the same rules for every contributor and bot.
Start with go vet and staticcheck, then layer golangci-lint with a committed .golangci.yml.
Adopt new checks incrementally on legacy repos so teams fix findings instead of disabling linters permanently.
Quick-reference recipe card - copy-paste ready.
go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
staticcheck ./...
golangci-lint run ./...Minimal .golangci.yml:
run:
timeout: 5m
linters:
enable:
- govet
- staticcheck
- ineffassign
- unused
issues:
max-issues-per-linter: 0When to reach for this:
go vet with correctness checks (staticcheck SA* rules).Problematic handler:
// example.com/demo/internal/api/handler.go
package api
import (
"fmt"
"net/http"
)
func Health(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ok") // missing error check
defer r.Body.Close() // Body may be nil on GET
}staticcheck locally:
staticcheck ./internal/api/...Typical findings include mishandled errors and suspicious constructs.
Wire CI with golangci-lint:
name: lint
on: [push, pull_request]
jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.26.x'
- run: go mod download
- uses: golangci/golangci-lint-action@v6
with:
version: latest
args: ./...Fixed handler:
func Health(w http.ResponseWriter, r *http.Request) {
if _, err := fmt.Fprintf(w, "ok"); err != nil {
http.Error(w, "write failed", http.StatusInternalServerError)
return
}
if r.Body != nil {
defer r.Body.Close()
}
}What this demonstrates:
go/analysis (directly or via wrappers) to inspect typed ASTs.max-issues-per-linter).| Category | Examples |
|---|---|
| Correctness | Impossible conditions, dubious comparisons, context misuse |
| Performance | Suboptimal append patterns, redundant conversions |
| Style | Deprecated API usage with suggested replacements |
Run staticcheck -explain SA1006 (example code) to read rule docs.
| Key | Purpose |
|---|---|
linters.enable / disable | Curate the bundle |
linters-settings | Per-linter config (e.g., gosec severity) |
run.build-tags | Match tagged builds in CI |
issues.exclude-rules | Narrow suppressions with path and text matchers |
issues.new-from-rev | Only fail on issues introduced after a baseline commit |
golangci-lint run ./... > baseline.txt and fix critical packages first.issues.new-from-rev: origin/main so legacy debt does not block every PR.nolint comments - require ticket IDs when exceptions are unavoidable.run:
go: "1.26"
build-tags:
- integrationAlign tags with test jobs that use -tags=integration.
nolint directives - //nolint:all hides future bugs. Fix: Name specific linters and justify in comment.go mod download before lint job.issues.exclude-rules only for known noisy paths.new-from-rev or temporary warning mode.| Alternative | Use When | Don't Use When |
|---|---|---|
go vet only | Tiny scripts, teaching repos | Production services needing deeper checks |
| staticcheck alone | Single linter, minimal config | You want gosec, misspell, and import lint in one run |
Custom go/analysis driver | One bespoke rule | Standard community coverage (use golangci-lint) |
| Review-only policy | Pre-lint culture | Scales poorly past a handful of engineers |
Optional - golangci-lint is enough in CI if staticcheck is enabled.
Some teams run staticcheck alone in pre-commit for speed.
Use a targeted issues.exclude-rules entry with path and linter keys.
Prefer fixing the code if the pattern is risky elsewhere.
It reports issues; formatting is gofmt/gofumpt.
Enable gofmt linter to fail on unformatted files.
Large monorepos need 5-10 minutes.
Start at 5m; increase if jobs cancel mid-run.
Exclude *.pb.go and vendor/ via run.skip-dirs or issues.exclude-rules.
Do not lint generated output you cannot change.
gopls embeds a subset for editor speed.
CI config is authoritative for merge requirements.
Yes if production code uses tags - mirror CI test tags in run.build-tags.
No, but it is the de facto standard orchestrator.
go vet plus staticcheck is a lighter minimum.
Run from each module root or use a meta-repo script looping go.mod paths.
go.work does not replace per-module CI jobs automatically.
Add gosec for security-sensitive services; nilaway when nil panics are frequent.
Enable via golangci-lint plugins or separate jobs - see the dedicated pages in this section.
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