Tooling: go vet, fmt and golangci-lint
Idiomatic Go isn't a matter of taste — it's enforced by tools. You'll use gofmt for canonical formatting, go vet and staticcheck to catch bugs the compiler allows, golangci-lint to run many linters at once, and go mod tidy / go build ./... to keep the module healthy — then wire it all into CI.
Learn Tooling: go vet, fmt and golangci-lint in our free Go course — an interactive lesson with worked examples, a practice exercise and a quick reference.
Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Canonical formatting with gofmt
Go has exactly one format. go fmt ./... rewrites your files; gofmt -l . lists files that aren't formatted (ideal for CI), and gofmt -d shows the diff. No more style arguments.
2️⃣ Catching bugs with go vet
go vet reports constructs that compile but are likely wrong — a classic example is a Printf verb that doesn't match its argument's type.
3️⃣ Many linters at once: golangci-lint
A .golangci.yml at the repo root enables the linters you want (govet, staticcheck, errcheck, unused, and more). One golangci-lint run ./... runs them all with caching.
4️⃣ Modules, builds, and CI
Round it out with go mod tidy (sync dependencies), go build ./... (compile everything), and run the whole set in CI so a bad change can't merge.
🎯 Your Turn
Fix a go vet finding. The format verb doesn't match the argument type — correct it as the comment shows.
❌ Committing unformatted code — noisy diffs and review friction.
✅ Run go fmt ./... (or save-on-format) and check gofmt -l in CI.
❌ Ignoring go vet warnings — they're usually real bugs.
✅ Treat vet as part of the build and fix every report.
❌ Forgetting go mod tidy — stale or missing requirements.
✅ Tidy after dependency changes; gate it in CI with git diff --exit-code .
❌ Enabling every linter blindly — a wall of noise nobody fixes.
✅ Start with a focused .golangci.yml and add linters deliberately.
go vet . gofmt only formats; vet reports suspicious-but-compiling code like bad Printf verbs.
golangci-lint run ./... , configured by .golangci.yml .
Run gofmt -l . , go vet ./... , go mod tidy , and golangci-lint run until every command exits 0 with no output.
Practice quiz
What does gofmt (and go fmt) do?
- rewrites your code into the canonical Go formatting
- runs your tests
- checks for race conditions
- downloads dependencies
Answer: rewrites your code into the canonical Go formatting. gofmt enforces one canonical formatting (tabs, spacing, alignment); go fmt runs gofmt over the packages.
What is go vet for?
- managing modules
- formatting source
- reporting suspicious constructs the compiler accepts but are likely bugs
- compiling for production
Answer: reporting suspicious constructs the compiler accepts but are likely bugs. go vet flags likely mistakes like bad Printf verbs, unreachable code, or copying a lock — things that compile but are wrong.
Which Printf mistake does go vet typically catch?
- a type that has no String method
- a format verb that doesn't match its argument, e.g. %d for a string
- an unused variable
- a missing import
Answer: a format verb that doesn't match its argument, e.g. %d for a string. vet's printf check flags mismatched verbs such as fmt.Printf("%d", "hi") where %d expects an integer.
What does go mod tidy do?
- runs all linters
- vendors dependencies
- formats go.mod
- adds missing and removes unused module requirements
Answer: adds missing and removes unused module requirements. go mod tidy syncs go.mod/go.sum with the imports actually used: adding what's needed and dropping what isn't.
What does go build ./... compile?
- every package in the module recursively
- just the tests
- the standard library
- only the main package
Answer: every package in the module recursively. The ./... pattern matches the current directory and all subdirectories, so it builds the whole module.
What is golangci-lint?
- a code formatter only
- a meta-linter that runs many linters together with shared config and caching
- Go's package manager
- a profiler
Answer: a meta-linter that runs many linters together with shared config and caching. golangci-lint aggregates dozens of linters (govet, staticcheck, ineffassign, ...) behind one config file and fast runner.
Where do you configure which linters golangci-lint runs?
- in main.go
- in the environment only
- in go.mod
- in a .golangci.yml (or .yaml/.toml) file
Answer: in a .golangci.yml (or .yaml/.toml) file. A .golangci.yml at the repo root enables/disables linters and sets their options; flags can override it.
What is staticcheck known for?
- generating mocks
- formatting imports
- a large set of high-quality correctness and simplification checks
- running benchmarks
Answer: a large set of high-quality correctness and simplification checks. staticcheck (the SA/ST/S checks) finds real bugs and suggests simplifications; it's bundled into golangci-lint.
Why does struct field alignment (field order) matter?
- it changes the JSON output
- reordering fields can reduce padding and shrink struct size in memory
- it speeds up compilation
- it is required for the code to compile
Answer: reordering fields can reduce padding and shrink struct size in memory. Because of memory alignment, grouping fields by size cuts padding; the fieldalignment analyzer can report wasteful layouts.
What is the best way to enforce these checks on every change?
- run gofmt -l, go vet, and golangci-lint in CI so a bad change fails the build
- only run them before a release
- rely on the IDE alone
- ask reviewers to run them by hand
Answer: run gofmt -l, go vet, and golangci-lint in CI so a bad change fails the build. Wiring fmt-check, vet, and golangci-lint into CI catches issues automatically on every push and pull request.