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.