go vet
How go vet detects suspicious constructs like Printf mismatches, bad struct tags, and unreachable code — what it catches, how to use it, and where it fits in your workflow.
What go vet Is
go vet is a static analysis tool that ships with the Go toolchain. It reads your source code and reports constructs that are technically valid but almost certainly wrong — things the compiler accepts but that will cause bugs at runtime or make the code behave unexpectedly. The tool uses heuristics, not a full formal verification. That means it looks for patterns that have repeatedly caused problems in real Go programs.
Heuristic, not proof:
A go vet warning is a strong signal that something is suspicious. It is not a guarantee of a bug. Treat each report as an item that needs a human decision: fix the code, or confirm the code is intentional and ignore the line.
Why go vet Exists
The Go compiler rejects many mistakes, but it deliberately stays out of the business of guessing intent. A call like fmt.Printf("%d", "hello") compiles without error — the function accepts ...interface{} — yet it will panic at runtime. Struct tags with missing quotes, unreachable code after a return, or shift operations that overflow the variable size are all legal Go that a compiler must accept.
Before go vet, these mistakes were discovered in code review or production. The tool was built to catch them automatically, as early as possible, with a near-zero false‑positive rate. That reliability is why the go test command runs go vet by default on test packages.
How go vet Works
go vet is built on the go/analysis framework. When you run it, a set of independent analyzers each examine the abstract syntax tree (AST) and type information of every package you specify. Each analyzer checks one narrow category of suspicious patterns: format strings, struct tags, shift counts, unreachable code, and others. The default set is tuned to produce very few false positives — you should almost never see a warning that is not worth investigating.
A beginner can think of go vet as a collection of small automated code reviewers. One reviewer only knows about Printf verbs. Another only knows about struct tags. They walk through your code and raise their hand when they spot a pattern they were trained to flag.
Using go vet
The most common invocation vets all packages in your module:
go vet ./...
You can also vet a specific package:
go vet ./cmd/server
If go vet finds nothing suspicious, it produces no output and exits with code 0. That silence is good — it means none of the built‑in analyzers triggered. You will get output only when there are warnings, and the process will exit with a non‑zero status so that CI pipelines can fail on it.
Silent vet run:
After running go vet ./..., if you see no output and the command returns successfully, all default checks passed. Your code has none of the suspicious patterns the built‑in analyzers look for.
The go test command also runs a subset of vet checks automatically. You can control that behavior with the -vet flag:
go test -vet=all ./... # run all vet checks on test packages (default)
go test -vet=off ./... # disable vet during testing
Beyond the defaults, you can run specific checks with flags. For example, the -shadow flag was once used for variable shadowing detection, but that check is no longer part of the standard go vet. Third‑party analyzers can be loaded with the -vettool flag, but that is an advanced topic.
Common Mistakes Detected by go vet
Each category below shows a real bug pattern and the vet output it produces.
Printf Format‑String Mismatches
The printf analyzer checks that each call to fmt.Printf, fmt.Sprintf, fmt.Fprintf, and similar functions has arguments that match the format verb.
package main
import "fmt"
func main() {
name := "Alex"
fmt.Printf("Welcome, %d\n", name)
}
Running go vet produces:
./main.go:8:2: fmt.Printf format %d has arg name of wrong type string
Runtime panic from format mismatches:
A %d verb with a string argument will cause a runtime panic: fmt.Printf("%d", "hello") panics with %!d(string=hello). Vet catches this at analysis time so it never reaches production.
The analyzer also catches missing arguments, extra arguments, and incorrect verb flags:
fmt.Printf("Value: %v %v\n", 42) // missing argument
fmt.Printf("Sum: %d\n", 1, 2, 3) // extra arguments
Struct Tag Validation
Go struct tags look like key:"value" and must follow a strict syntax. A common mistake is forgetting the quotes around the value, or misplacing colons.
type User struct {
Name string `json:name` // missing quotes
Age int `json:"age" xml:"age"` // valid
}
Vet reports:
./main.go:5:2: struct field tag `json:name` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
Broken tags silently break serialization:
A malformed struct tag causes encoding/json and similar packages to ignore the tag entirely, falling back to the field name. Your JSON output will use Name instead of name, and you might not notice until an integration test fails.
Vet also warns about duplicate tag keys or keys that are not well‑formed.
Unreachable Code
Code after a return, break, continue, or panic that can never execute is flagged.
func divide(a, b int) int {
if b == 0 {
panic("division by zero")
return 0 // unreachable
}
return a / b
}
./main.go:5:2: unreachable code
This often indicates a logic error — a leftover line after a refactor, or a mistaken assumption about control flow.
Suspicious Shift Operations
Shifting an integer by a number larger than its bit width is legal Go but almost never intended.
var x int32 = 1 << 33
Vet warns:
./main.go:3:15: suspicious shift of 33 bits in int32
Because int32 has 32 bits, shifting by 33 is a mistake. The result is effectively 0 because the shift count is taken modulo the bit size, which is surprising.
Unkeyed Composite Literals
Vet’s composites analyzer flags struct literals that omit field names, which can lead to brittle code if the struct definition later changes.
type Point struct{ X, Y int }
p := Point{10, 20} // unkeyed
./main.go:4:11: github.com/your/module.Point composite literal uses unkeyed fields
Using Point{X: 10, Y: 20} makes the intent explicit and protects against future reordering of fields.
Unkeyed literals are not always wrong:
The standard library uses unkeyed literals deliberately in some generated code. Vet’s warning is a prompt to consider whether a keyed literal would be safer. If the unkeyed form is intentional, you can suppress the warning with a comment, but in most application code you should use field names.
Other Built‑in Checks
The default analyzers also detect:
- Incorrect arguments to
testing.Tandtesting.Blogging methods (e.g., usingt.Fatalwhent.Fatalfwas intended) - Mistakes with
unsafe.Pointerrules when the-unsafeptrflag is enabled (off by default, as it is more prone to false positives) - Boolean conditions that are always true or always false through the
boolanalyzer
The exact set of checks can be listed with go doc cmd/vet.
Interpreting go vet Output
Every report follows the pattern:
file:line:col: message
For example:
./handler.go:42:3: fmt.Sprintf format %s reads arg #1, but call has 0 args
The file path is relative to the current directory. The line and column pinpoint where the suspicious construct begins. The message describes the specific rule that was violated and enough context to understand why.
Because vet uses heuristics, not every report is an actual bug. A format string that is built dynamically at runtime, for example, cannot be fully verified and may produce a false positive. In those cases, you can add a //lint:ignore comment directive (if using certain analyzers) or restructure the code to make the intent clearer. Do not ignore vet warnings without at least understanding why they appeared.
Fitting go vet into Your Workflow
Running vet manually before committing catches many issues that would otherwise slip through review. Most teams automate it in three places:
- Local development: Run
go vet ./...before creating a pull request. It takes seconds on typical projects. - Continuous integration: Add
go vet ./...as a step in your CI pipeline. Because the command exits non‑zero on any warning, it naturally fails the build. - Testing: The default
go testbehavior already includes vet checks. Usego test -vet=allto ensure no checks are silently skipped.
If your project uses a Makefile, a typical target looks like:
vet:
go vet ./...
You can then run make vet as part of your pre‑commit routine.
Common Misconceptions
go vet is often confused with other tools. Here is where it fits:
- Not a full linter:
go vetcatches a specific, curated set of mistakes. It does not check for style, complexity, naming conventions, or unused variables (that isgo fmtandgolintterritory). For comprehensive linting, you would pairgo vetwith a tool likestaticcheckorgolangci-lint. - Not a replacement for tests: Vet catches static patterns. It cannot verify logic, business rules, or concurrent safety.
- Silence does not mean perfection: No warnings means no matches for the built‑in heuristics. It does not mean the code is correct.
- Vet runs on test files by default: Many developers assume
go vetonly checks production code, but it also analyzes_test.gofiles. A broken format string in a test helper will be flagged just like one in main code.
Summary
go vet is a fast, reliable static checker that targets the small set of mistakes the Go compiler deliberately ignores. Its strength is its precision: you can run it on every build, on every commit, and trust that a warning almost always points to something worth fixing. Making go vet ./... part of your regular workflow catches format‑string panics, broken struct tags, unreachable code, and more — before they become runtime failures.