go vet — Find the Bug¶
Each scenario shows code that compiles cleanly but is wrong. Vet catches every one of them. Identify the defect, explain it, and fix it.
Bug 1 — printf verb/argument mismatch¶
Bug: %d formats an integer, but the argument is a string. The program runs but prints garbage like count = %!d(string=forty-two). Fix: match verb to type — %s for string, %d for int — or convert: fmt.Printf("count = %s\n", "forty-two").
Bug 2 — lost cancel from context.WithCancel¶
func work(parent context.Context) {
ctx, _ := context.WithCancel(parent) // cancel discarded
doStuff(ctx)
}
./main.go:12:14: the cancel function returned by context.WithCancel should be called, not discarded, to avoid a context leak
Bug: discarding the cancel function leaks the context's goroutine/resources until parent is cancelled. Fix: capture and defer it:
Bug 3 — resp.Body not closed (and used before err check)¶
Bug: if http.Get returns an error, resp may be nil and resp.Body.Close() panics. The defer runs unconditionally before the err check. Fix: check the error first, then defer the close:
Bug 4 — malformed struct tag¶
./main.go:3:9: struct field tag `json:user_id` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
Bug: struct tag values must be quoted strings: json:"user_id". Without quotes, encoding/json silently treats the field as having no tag, so JSON marshalling uses the field name ID instead of user_id — a silent serialization bug. Fix: quote the value: `json:"user_id"`.
Bug 5 — variable shadowing inside a loop¶
var err error
for _, item := range items {
if err := process(item); err != nil { // new err shadows outer
log.Println(err)
continue
}
}
return err // always nil
(Reported when shadow is enabled: go vet -vettool=$(which shadow) ./... or via the shadow analyzer.)
Bug: the inner err := creates a new variable that vanishes at end of the if; the outer err is never assigned, so return err always returns nil. Fix: either reuse the outer err (err = process(item)) or rename the inner one and propagate intentionally.
Bug 6 — errors.As with a non-pointer target¶
./main.go:5:16: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
Bug: errors.As writes into its second argument, so it must be a pointer. Passing the value causes a runtime panic. Fix:
Bug 7 — unreachable code after return¶
Bug: the second return can never execute. Often a leftover from refactoring; sometimes signals dead branches where someone forgot to delete old code. Fix: remove the unreachable statement.
Bug 8 — fmt.Errorf without %w¶
(Reported by the errorsas/fmtwrap-style analyzers in newer Go versions.)
Bug: using %s (or %v) discards the original error chain — callers cannot errors.Is or errors.As back to the original. Use %w to wrap. Fix:
Bug 9 — copying a struct that contains a sync.Mutex¶
type Counter struct {
mu sync.Mutex
n int
}
func (c Counter) Inc() { c.mu.Lock(); c.n++; c.mu.Unlock() } // value receiver!
Bug: the value receiver copies the mutex on every call, so each call locks its own mutex — no synchronization actually happens. Worst: it compiles and tests may even pass by luck under low concurrency. Fix: use a pointer receiver:
Bug 10 — composite literal across packages without field names¶
Bug: positional composite literals across package boundaries break silently when the imported package adds/reorders fields. Fix: name the fields:
How to approach these¶
- Read the diagnostic literally — vet messages name the file, line, and exact issue.
- Vet never lies (zero false positives) — assume the code is wrong, not the tool.
- Look at types and lifetimes: most vet bugs involve a type/verb mismatch, a copied-vs-shared value, or a discarded resource.
- If you suspect vet is wrong, reproduce in a minimal file and double-check against
go doc cmd/vet; in practice the bug is always in the code. - Wire vet into CI so these never reach
mainin the first place.