Day 64 — Vet, staticcheck & module hygiene
Day 64 — Vet, staticcheck & module hygiene
Stage VII · ~3h
Goal: Run go vet and staticcheck to a clean baseline, understand what each catches, and document private-module / replace discipline so CI can enforce quality without drama.
Why this day exists
Tests prove behavior you wrote assertions for. Static analysis catches whole classes of mistakes tests often miss:
- Unreachable or suspicious control flow
- Misused standard library APIs
- Useless assignments, broken error checks, deprecated patterns
- Module layout mistakes that only appear when someone else clones the repo
Ship quality means the gate is mechanical, not “I eyeballed it.”
Theory 1 — go vet (built-in)
go vet is part of the toolchain. It runs analyzers on packages you name:
go vet ./...It is intentionally conservative: few false positives, limited depth. Examples of classic findings:
Printfformat/arg mismatches
- Unreachable code after
return
- Copying a mutex by value
- Suspicious
cancel()not called on context
- Testing flags misuse
Not a full linter suite. Treat it as the free baseline every project runs.
Example — format bug vet can catch
package main
import "fmt"
func main() {
id := 42
fmt.Printf("user=%s\n", id) // vet: wrong type for %s
}go vet .
# fmt.Printf format %s has arg id of wrong type intTheory 2 — staticcheck (industry default)
staticcheck is the most widely adopted third-party static analyzer for Go. It groups checks as SAxxxx, STxxxx, SIxxxx, Uxxxx, etc.
Install (pin a version in real teams; for learning, current is fine):
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -version
staticcheck ./...What it adds beyond vet
| Area | Examples |
|---|---|
| Correctness | nil deref risk patterns, useless conditionals |
| Style / simplicity | simplify boolean expressions, unused code |
| API misuse | deprecated stdlib, wrong io usage |
| Performance hints | some alloc / append patterns (not a substitute for benchmarks) |
Configuration — staticcheck.conf
At module root:
# staticcheck.conf
checks = ["all", "-ST1000", "-ST1003"]Or via CLI:
staticcheck -checks=all,-ST1000 ./...Start with defaults, then disable only with a written reason. Blanket checks = [] is not a quality bar.
Example — SA4006 (ineffective assignment)
func normalize(s string) string {
s = strings.TrimSpace(s)
s = strings.ToLower(s)
s = s // staticcheck may flag pointless reassignment patterns
return s
}More realistic: assigning err and overwriting without checking:
func load(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = f.Stat()
// forgetting to check err here is a common bug class analyzers push you to fix
return err
}Theory 3 — Layering analysis in CI
Recommended order for this volume’s mental model:
go test ./...
go vet ./...
staticcheck ./...
govulncheck ./... # Day 72
Optional later: golangci-lint as an orchestrator. Prefer understanding individual tools first so you can debug false positives.
Makefile / script sketch
#!/usr/bin/env bash
set -euo pipefail
go test ./...
go vet ./...
staticcheck ./...Theory 4 — Module private / replace hygiene
GOPRIVATE / GONOSUMDB / GONOPROXY
Private modules must not hit the public proxy/sumdb:
go env -w GOPRIVATE=github.com/yourorg/*,example.com/internal/*
# often also:
# GONOSUMDB=$GOPRIVATE
# GONOPROXY=$GOPRIVATEWithout this, go mod download fails weirdly on private VCS or leaks intent to public infrastructure.
replace directives
// go.mod
module example.com/day64
go 1.26
require example.com/lib v1.2.3
replace example.com/lib => ../lib| Use | Abuse |
|---|---|
| Local multi-module monorepo development | Shipping replace to a laptop path in a public module |
| Temporary fork while waiting on upstream | Permanent silent fork nobody notices |
Rule of thumb: replace for local work is fine; CI must either vendor the story or use published versions. Document every replace in README.
go mod tidy discipline
go mod tidy
go mod verifyTidy removes unused requires and adds missing ones. Commit go.mod and go.sum together.
Worked example — clean a sloppy package
Before (intentional issues for the lab):
package greeter
import (
"fmt"
"strings"
)
func Hello(name string) string {
name = strings.TrimSpace(name)
if name == name {
// always true — staticcheck ST / SA style finding depending on version
}
return fmt.Sprintf("hello, %s", name)
}
func unused() {} // U1000 if unexported and unusedAfter:
package greeter
import "strings"
func Hello(name string) string {
name = strings.TrimSpace(name)
if name == "" {
name = "world"
}
return "hello, " + name
}go test ./...
go vet ./...
staticcheck ./...Lab
Suggested workspace: ~/lab/90daysofx/01-go/day64 (or continue your Day 62 service module).
- Install staticcheck; record version in
NOTES.md.
- Run
go vet ./...andstaticcheck ./...on a non-trivial module (prefer Stage VI service if you have it).
- Fix all findings or document suppressions with
//lint:ignoreonly when justified (rare).
- Add a
scripts/lint.sh(or Makefile target) that runs test + vet + staticcheck.
- Write 5–10 lines on your
GOPRIVATE/replacepolicy for this repo (even if “none — all public modules”).
Stretch
- Enable
checks = ["all"]and triage noise.
- Compare one finding that vet misses and staticcheck catches; paste both tool outputs in NOTES.
Common gotchas
| Gotcha | Fix |
|---|---|
| staticcheck not on PATH | $(go env GOPATH)/bin must be on PATH |
| Different results in CI | Pin staticcheck version; same Go version |
| Ignoring entire check families | Prefer fixing code; disable one code with reason |
Committing laptop replace |
Use published module or CI-visible path |
go mod tidy fight |
One owner commits tidy; avoid partial edits |
| Treating lint green as “secure” | Still run govulncheck (Day 72) and review auth |
Checkpoint
go vet ./...clean
staticcheck ./...clean (or documented exceptions)
- Lint script committed
- Module policy note (
GOPRIVATE/replace) written
- Understand one SA/ST finding you fixed
Commit
git add .
git commit -m "day64: vet staticcheck module hygiene"Write three personal gotchas before continuing.
Tomorrow
Day 65 — pprof: capture CPU and heap profiles and extract evidence before you rewrite anything for speed.