go vet & Common Analyzer Diagnostics
go vet inspects Go source for constructs that compile but are probably wrong.
Every team should run go vet ./... locally and in CI, understand the common diagnostics, and know when deeper analyzers add value.
Recipe
Quick-reference recipe card - copy-paste ready.
# All packages in module
go vet ./...
# Specific packages
go vet ./internal/...
# List available analyzers (Go 1.26+)
go tool vet help
# Run vet via test harness (some teams prefer)
go test -vet=off ./... # disable vet during test when running separatelyWhen to reach for this:
- CI baseline before adding golangci-lint.
- Debugging mysterious printf or JSON tag failures.
- After upgrading Go minor versions (read release notes for new vet checks).
- Teaching juniors why code "compiles but smells wrong."
Working Example
Buggy handler:
package api
import (
"encoding/json"
"fmt"
"net/http"
)
type User struct {
Name string `json:name` // invalid tag syntax
}
func Handler(w http.ResponseWriter, r *http.Request) {
u := User{Name: "ada"}
_ = json.NewEncoder(w).Encode(u)
fmt.Printf("user %d", u.Name) // format verb mismatch
}Run:
go vet ./...Typical output:
api/handler.go:12:2: fmt.Printf format %d has arg u.Name of wrong type string
api/handler.go:8:5: struct field tag `json:name` not compatible with reflect.StructTag.Get: bad syntax for struct tag pairFixed code:
type User struct {
Name string `json:"name"`
}
func Handler(w http.ResponseWriter, r *http.Request) {
u := User{Name: "ada"}
_ = json.NewEncoder(w).Encode(u)
fmt.Printf("user %s", u.Name)
}What this demonstrates:
- Vet catches Printf verb mistakes without running the program.
- structtag analyzer validates JSON tag syntax before marshaling surprises in production.
- Fixes are source changes, not linter suppressions.
Deep Dive
How It Works
- Vet loads packages using the same build context as
go test. - Each analyzer is a function registered with the vet driver (many built on
go/analysis). - Diagnostics include file, line, and a human-readable message.
- Build tags,
GOOS, andGOARCHaffect which files vet examines.
Common Vet Analyzers (Representative)
| Analyzer | What it flags | Example fix |
|---|---|---|
printf | Format verbs vs argument types | Match %s, %d, %v to args |
structtag | Malformed or conflicting struct tags | Fix json:"name" quoting |
unsafeptr | Invalid unsafe.Pointer conversions | Redesign unsafe code or add tests |
copylocks | Copying structs containing locks | Use pointer receivers or avoid copy |
loopclosure | Goroutine captures loop vars (pre-1.22 footgun) | Pass param or use Go 1.22+ semantics |
lostcancel | context.WithCancel without cancel() call | defer cancel() |
httpresponse | http.Response body not closed on error paths | defer resp.Body.Close() with err check |
testinggoroutine | t.Fatal from non-test goroutine | Signal failure via channel or t.Error + sync |
Relationship to golangci-lint
golangci-lint's govet linter wraps vet analyzers and may enable additional checks.
Running go vet alone is simpler in scripts; golangci-lint centralizes severity and excludes.
# .golangci.yml excerpt
linters:
enable:
- govet
linters-settings:
govet:
enable-all: trueGo Notes
// copylocks example - vet flags copying sync.Mutex
type Bad struct {
mu sync.Mutex
}
func use(b Bad) { // vet: passes lock by value
b.mu.Lock()
}Prefer pointer receivers or do not embed locks in copied structs.
When Vet Is Not Enough
| Need | Reach for |
|---|---|
| Dead code, API deprecation | staticcheck (SA*) |
| Nil dereference paths | nilaway, staticcheck |
| Security smells | gosec |
| Custom org APIs | go/analysis plugin |
Gotchas
- Assuming vet is unchanged across Go upgrades - New minors add checks; CI may fail on legacy code. Fix: Read release notes; run
go veton upgrade branch early. - Running vet only on
mainpackages - Bugs hide ininternal/. Fix: Alwaysgo vet ./.... - Ignoring
copylocksin hot paths - Passing locked structs copies mutex state. Fix: Use pointers or redesign API. - Suppressing printf warnings - Almost always a real bug. Fix: Correct the verb or use
%vdeliberately with comment. - Mismatched build tags between editor and CI - Vet skips files CI does not see. Fix: Align tags in gopls settings and CI
GOFLAGS. - Using
//nolint:govetbroadly - Hides future real issues. Fix: Narrow suppressions to one line with justification ticket.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| golangci-lint only | Need one config for 30+ linters | You want zero YAML and fast scripts |
| staticcheck alone | Deeper correctness, SA codes | You need official toolchain checks in minimal CI |
| Compiler errors only | Never for production Go | - |
| Runtime tests only | Complements vet | Replacing static analysis for tag/printf bugs |
FAQs
Does go vet catch all bugs?
No.
It targets high-confidence suspicious patterns.
Tests, race detector, and staticcheck fill other gaps.
Should vet run before or after tests in CI?
Order is flexible.
Many pipelines run go vet and go test in parallel jobs to save wall time.
Why does vet pass locally but fail in CI?
Different Go versions, build tags, or files only present in CI checkout.
Align go version and environment variables.
What is the loopclosure analyzer about?
Before Go 1.22, loop variables were reused, so goroutines saw the final value.
Go 1.22+ creates per-iteration variables; still worth understanding for libraries supporting older Go.
How is structtag different from json.Marshal errors?
structtag fails at compile/vet time for malformed tags.
Marshal errors often appear only at runtime for unsupported field types.
Can I disable one vet analyzer?
In golangci-lint, tune govet settings.
Plain go vet uses the toolchain default set for your Go version.
Does vet analyze test files?
Yes when you vet packages that include tests.
Some analyzers have test-specific rules (testinggoroutine).
Should generated code be vetted?
Yes if it is compiled.
If generators emit known vet noise, exclude paths in golangci-lint rather than skipping vet entirely.
How does vet relate to go fix?
Separate tools.
go fix applies rewrites; go vet reports problems without auto-fix (except via editor quick fixes for some analyzers).
Is unreachable code a vet finding?
Yes (unreachable analyzer).
Often indicates missing return or dead branches after refactor.
Related
- Go Code Quality: Opinionated by Design - quality stack overview
- Code Quality Basics - first vet invocation
- golangci-lint Configuration - govet settings
- staticcheck & golangci-lint in CI - deeper analyzers
- CI Quality Gates: Lint, Test, Race, Vuln - vet in pipeline
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).