Skip to content

Day 050 — go vet & golangci-lint

Month 2 · Week 4 · ⬅ Day 049 · Day 051 ➡ · Journal index

🎯 Learning Objective

Catch bugs the compiler won't, using go vet (built in) and golangci-lint (an aggregator), and wire them into a reproducible workflow.

📚 Topics

  • go vet analyzers and what each one catches
  • staticcheck vs golangci-lint (meta-linter)
  • Configuring linters with .golangci.yml; suppressing false positives

📖 Reading / Sources

📝 Notes

  • go vet ships with the toolchain and runs a curated set of analyzers (the golang.org/x/tools/go/analysis framework). It reports correct-but-suspicious code: Printf verb/arg mismatches, struct tags that won't parse, sync.Mutex copied by value, unreachable code, shadowed err (via -vetshadow/shadow), lost context.CancelFunc, loop-variable capture, etc. → links to [[error-handling]] and [[printf-verbs]].
  • go test automatically runs a subset of go vet before tests; a vet failure fails the test run. So clean vet output is table stakes, not optional.
  • go vet is a checker, not a fixer — it never modifies code. Pair it with gofmt/goimports (which do rewrite).
  • golangci-lint is a fast meta-linter that runs many linters in one pass with shared parsing/caching. Default set includes govet, staticcheck, errcheck (unchecked errors), ineffassign, unused, and more. It is a third-party binary you run as a tool — never an import — so it doesn't affect what your program compiles to.
  • staticcheck is the highest-signal linter: SA-checks find real bugs (e.g. SA4006 value never used, SA1029 wrong context key type), ST-checks are style. golangci-lint bundles it.
  • Suppress a single false positive with a //nolint:lintername // reason comment — narrowly, with a reason, never blanket-disabled.
  • The classic vet catch: fmt.Printf("%d", "hello") compiles fine (variadic any) but is a bug; vet flags the verb/type mismatch.

💻 Code Examples

// vet flags every line below — all compile cleanly.
fmt.Printf("%d\n", "not a number") // %d expects int, got string
type T struct {
    Name string `json:Name` // bad struct tag: missing quotes -> json:"Name"
}
var mu sync.Mutex
locked := mu // copies a Mutex by value -> "assignment copies lock value"
_ = locked

A minimal .golangci.yml:

linters:
  enable:
    - errcheck
    - govet
    - staticcheck
    - ineffassign
    - unused
run:
  timeout: 2m

No runnable example: golangci-lint/staticcheck are external binaries, not importable stdlib. See the tooling drills in exercises/month-02/week-4/.

🏋️ Exercises / Practice

Exercise Status Link
Run go vet ./... on the week's exercises exercises/month-02/week-4
Add a deliberate Printf bug, confirm vet catches it exercises/month-02/week-4

🐛 Mistakes Made

  • Assumed go vet would auto-fix formatting — it doesn't; only reports. gofmt -w fixes formatting.
  • Tried import "github.com/golangci/golangci-lint" — it's a CLI tool, not a library; install the binary and run it.

❓ Open Questions

  • Which linters are worth enabling beyond the default set without drowning in noise? (Leaning: errcheck, gosec, revive selectively.)

🧠 Active Recall (answer without looking)

  1. Q: Why can fmt.Printf("%d", "x") compile yet still be a bug, and what catches it?
    A

Printf takes ...any, so any argument type compiles. The format string is a runtime contract the compiler doesn't check. go vet's printf analyzer matches verbs to arg types statically and flags the mismatch. 2. Q: Does go test run go vet?

A

Yes — go test runs a high-confidence subset of vet analyzers before building/running tests, and a vet error fails the test run. Disable with go test -vet=off (rarely a good idea).

🪶 Feynman Reflection

The compiler proves your code is legal Go; vet and linters argue it's probably correct Go. They read patterns a human reviewer would flag — a format verb that doesn't match its argument, an error you forgot to check, a mutex you accidentally copied — and surface them before they become production incidents.

🕳️ Knowledge Gaps

  • Writing a custom analysis.Analyzer — deferred; consuming existing ones is enough for now.

✅ Summary

I can run go vet and golangci-lint, read their output, configure a sane linter set, and suppress false positives narrowly with //nolint.

⏭️ Next Steps / Prep for Tomorrow

  • Day 051: capture these commands in a Makefile so the whole workflow is one make away.

Time spent Difficulty Confidence
90 min 🟦🟦⬜⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: docs(journal): go vet and golangci-lint (day 050)