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.T and testing.B logging methods (e.g., using t.Fatal when t.Fatalf was intended)
  • Mistakes with unsafe.Pointer rules when the -unsafeptr flag is enabled (off by default, as it is more prone to false positives)
  • Boolean conditions that are always true or always false through the bool analyzer

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 test behavior already includes vet checks. Use go test -vet=all to 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 vet catches a specific, curated set of mistakes. It does not check for style, complexity, naming conventions, or unused variables (that is go fmt and golint territory). For comprehensive linting, you would pair go vet with a tool like staticcheck or golangci-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 vet only checks production code, but it also analyzes _test.go files. 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.