Day 64 — Vet, staticcheck & module hygiene

Updated

July 30, 2026

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:

  • Printf format/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 int

Theory 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=$GOPRIVATE

Without 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 verify

Tidy 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 unused

After:

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).

  1. Install staticcheck; record version in NOTES.md.
  2. Run go vet ./... and staticcheck ./... on a non-trivial module (prefer Stage VI service if you have it).
  3. Fix all findings or document suppressions with //lint:ignore only when justified (rare).
  4. Add a scripts/lint.sh (or Makefile target) that runs test + vet + staticcheck.
  5. Write 5–10 lines on your GOPRIVATE / replace policy 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.