staticcheck — Find the Bug¶
Each scenario shows code or a setup that staticcheck flags (or that misuses staticcheck itself). Find the defect, explain it, and fix it.
Bug 1 — SA1006: Printf verb mismatch¶
Bug: %d expects an integer; name is a string. At run time you get %!d(string=Ada) instead of the name. Fix: use the right verb: fmt.Printf("%s\n", name) or fmt.Println(name).
Bug 2 — SA4006: assigned value never used¶
func price(items []Item) int {
total := 0
total = sum(items) // first total := 0 was pointless
return total
}
Bug: the initial assignment total := 0 is overwritten before it is read. staticcheck reports SA4006: this value of total is never used. Fix: declare and assign in one step, removing the dead store: total := sum(items); return total (or just return sum(items)).
Bug 3 — SA1019: deprecated API use¶
Bug: ioutil.ReadFile is deprecated since Go 1.16. The package documentation says so, and SA1019 flags every call site. Fix: use the modern equivalent in os: os.ReadFile("config.yaml"). Same signature, no deprecation.
Bug 4 — SA9003: empty branch¶
Bug: an empty if body is almost always either an unfinished feature or a typo (someone meant to negate the condition). SA9003 catches it. Fix: either implement the branch or invert the condition and put the real logic on the other side. Do not leave empty branches in committed code; add a real action or remove the conditional.
Bug 5 — SA5007: infinite recursive call¶
func (n *Node) String() string {
return fmt.Sprintf("Node(%s)", n) // calls String on n again via %s
}
Bug: fmt.Sprintf("%s", n) invokes n.String(), which calls itself with no termination — a stack overflow at run time. staticcheck's SA5007/related infinite-recursion analyzers flag it. Fix: print a primitive field instead of the receiver: return fmt.Sprintf("Node(%s)", n.Name) (assuming Name is the relevant field).
Bug 6 — S1005: redundant blank in range¶
Bug: the second blank is unnecessary; for _ = range items (or simply for range items) is idiomatic. Fix: drop the second variable: for range items { count++ }. S1005 suggests exactly this simplification.
Bug 7 — ST1005: capitalized error string¶
Bug: Go convention is lowercase, no trailing punctuation, because errors are often wrapped (fmt.Errorf("read config: %w", err)) and a capital letter mid-sentence looks wrong. ST1005 enforces this. Fix: lowercase and trim punctuation: return errors.New("failed to open file").
Bug 8 — U1000: unused unexported function¶
package billing
func computeTax(amount int) int { ... } // never called
func ComputeTotal(items []Item) int { ... }
Bug: computeTax is unexported and never referenced; U1000 reports it as unused dead code. Fix: delete it. Dead code rots — it is not tested and slowly drifts from the rest of the package. If you genuinely need it later, restore from git history. (Note: unused can only flag unexported symbols; exported ones may have callers outside the analyzed set.)
Bug 9 — //lint:ignore without a reason¶
Bug: the ignore directive has no justification. Future readers cannot tell whether the deprecation was acknowledged on purpose or whether someone silenced the linter to land a PR. Bare ignores accumulate and become permanent. Fix: include a reason: //lint:ignore SA1019 v2 API does not yet support TLS resumption; migrate after #1234. Reject PRs in review that add bare ignores.
Bug 10 — Version drift from @latest in CI¶
- name: staticcheck
run: |
go install honnef.co/go/tools/cmd/staticcheck@latest
staticcheck -set_exit_status ./...
Bug: @latest resolves to whatever release is newest at the moment CI runs. A new staticcheck release (added check or refined analyzer) can suddenly break the build on a PR that did not touch any flagged code. Fix: pin a version and upgrade deliberately in its own PR:
- run: go install honnef.co/go/tools/cmd/staticcheck@v0.5.1
- run: staticcheck -set_exit_status ./...
Treat the version like a dependency: review diffs in findings when bumping it.
How to approach these¶
- Read the ID first — the family (
SA,S,ST,U) tells you whether to fix urgently, refactor, or debate style. - If unsure,
staticcheck -explain SAXXXXin the terminal before silencing. - A
//lint:ignorewithout a reason is a smell — every ignore needs justification. - CI flakes after no code change? Check whether staticcheck (or any tool) is pinned.
unusedflags exported symbols? It cannot — anything it flags is genuinely unreachable from the analyzed roots.