The compiler catches syntax. vet catches logic.
You write a function that formats a log message. The compiler accepts it without complaint. The program compiles, links, and runs. But every time it prints, the timestamp appears twice and the error code shows as %!d(string). The syntax is perfect. The logic is broken. This is exactly where go vet steps in. It catches the mistakes that pass the compiler but will misbehave at runtime.
What vet actually does
go vet is a static analysis tool built directly into the Go toolchain. It does not check syntax. It checks semantics. Think of the compiler as a grammar teacher who marks missing brackets and type mismatches. Think of vet as a logic coach who points out that you passed a string to a function expecting an integer, or that you accidentally created a new variable inside a loop that shadows the one you actually meant to use. It analyzes your code without running it, walking through the abstract syntax tree to find patterns that almost always indicate a bug.
The tool focuses on high-confidence checks. It does not care about naming conventions, line length, or whether you prefer tabs or spaces. Those belong to gofmt and external linters. vet cares about correctness. It looks for unreachable code, format string mismatches, nil pointer dereferences, and loop variable captures. It is opinionated by design, but its opinions are grounded in decades of systems programming experience. The Go community accepts this narrow scope because it keeps the tool fast and reliable. You do not spend time configuring rules. You run it, read the output, and fix the code.
Trust the compiler for syntax. Trust vet for logic.
Running your first check
Here is the simplest way to see vet in action. Create a package with a format string mismatch and a shadowed variable.
package main
import "fmt"
// main demonstrates two common vet targets: printf mismatches and variable shadowing.
func main() {
// fmt.Printf expects a format string. Passing a plain string triggers vet.
fmt.Printf("status: %d\n", "active")
// The loop creates a new err variable that shadows the outer one.
err := doWork()
if err != nil {
// vet catches this because the inner err is never used.
err := recoverWork()
fmt.Println(err)
}
}
// doWork returns a dummy error for demonstration.
func doWork() error {
return nil
}
// recoverWork returns a dummy error for demonstration.
func recoverWork() error {
return nil
}
Run the tool from the terminal.
# Analyze the current package and print findings to stderr.
go vet ./...
The output points directly to the problematic lines. It tells you exactly which format verb does not match the argument type, and it flags the shadowed variable. You do not need to install anything. The binary ships with every Go distribution.
Run vet before you run your tests. Catch the silly mistakes while the code is still fresh.
How the analysis runs
Under the hood, go vet uses the go/analysis framework. The tool parses your source files into an abstract syntax tree. It does not generate machine code. It does not link against external libraries. It simply reads the structure of your code and runs a series of predefined passes. Each pass looks for a specific pattern. The printf pass validates format strings against argument types. The nilfunc pass checks for function values that are always nil. The shadow pass tracks variable declarations to ensure you are not accidentally hiding an outer variable with an inner one.
The analysis happens at the package level. When you run go vet ./..., the tool resolves your module dependencies, parses every file in the matched packages, and runs the passes in parallel. Because it skips compilation and linking, it finishes in milliseconds for small projects and seconds for large ones. The speed comes from analyzing the AST rather than executing the program. You get immediate feedback without waiting for a full build cycle.
The tool also respects build constraints. It skips files tagged for other operating systems or architectures. It follows the same import resolution rules as go build. If a package fails to compile, vet stops and reports the compilation error first. You cannot analyze broken syntax. Fix the compiler errors, then run vet to catch the logic errors.
Vet reads your code structure. It does not run your code. Keep that distinction clear.
Real-world usage: custom formatters and CI
Real projects rarely use the default flags. You usually need to tell vet about custom logging functions or wrap it in a CI script. Here is how a typical logging wrapper looks and how you configure vet to understand it.
package logger
import (
"fmt"
"time"
)
// Log formats a message and writes it to standard output.
func Log(format string, args ...interface{}) {
// Prefix the output with a timestamp.
fmt.Printf("[%s] %s\n", time.Now().Format("15:04:05"), fmt.Sprintf(format, args...))
}
The standard printf check only knows about fmt.Printf, fmt.Sprintf, and a few others. It does not know about your logger.Log function. You teach it using the -printfuncs flag.
# Tell vet to treat logger.Log as a printf-style function.
go vet -printfuncs="logger.Log" ./...
Now vet validates the format string inside Log just like it validates fmt.Printf. You can chain multiple functions by separating them with commas. The flag accepts any function signature that matches the printf pattern.
Integrating vet into a continuous integration pipeline follows the same pattern. You want the build to fail if vet finds a problem. You also want to run it before compilation to save time.
# Fail the pipeline if vet reports any issues.
go vet ./... || exit 1
The tool writes findings to standard error and returns a non-zero exit code when it detects a problem. That behavior fits perfectly into shell scripts, Makefiles, and GitHub Actions. You do not need a separate step to parse the output. The exit code tells your CI runner whether to proceed or abort.
The original documentation sometimes suggests using inline comments to suppress warnings. go vet does not support suppression comments. If you see a warning, you fix the code. If the warning is a false positive, you adjust your function signature or refactor the logic. External linters use //nolint, but vet stays strict. It forces you to confront the pattern rather than hide it.
Fix the warning. Do not suppress it. The tool exists to make bugs impossible to ignore.
Pitfalls and compiler interactions
Vet is fast and accurate, but it is not a replacement for testing or manual review. It focuses on a narrow set of checks. It will not catch race conditions, memory leaks, or business logic errors. It also does not enforce style guidelines. If you want to check for cyclomatic complexity, unused parameters, or naming conventions, you need a linter like golangci-lint. Vet handles correctness. Linters handle style. Keep them separate.
You will occasionally run into false positives. The shadow check sometimes flags variables that are intentionally redeclared in a narrower scope. The printf check struggles with variadic functions that change their signature at runtime. When this happens, the compiler rejects the program with format %d has arg %s of wrong type string or declaration of err shadows declaration at line 10. Read the message carefully. If the code is actually correct, refactor it to make the intent obvious. Rename the inner variable. Split the function. Vet prefers clarity over cleverness.
Another common mistake is running vet on a directory that contains generated code. Generated files often contain repetitive patterns that trigger warnings. Exclude them from your analysis or regenerate them after fixing the source. The tool does not distinguish between hand-written and machine-generated code. It treats every .go file the same way.
Performance is rarely an issue, but massive monorepos can slow down the initial pass. The tool caches results in your module cache. Subsequent runs finish instantly unless you change the source files. Clear the cache with go clean -cache if you suspect stale results. The cache stores the AST and analysis state, not the compiled binaries.
Vet catches patterns. Tests catch behavior. Use both.
Choosing your analysis tools
Use go vet when you want fast, built-in correctness checks without installing third-party tools. Use golangci-lint when you need comprehensive style enforcement, complexity metrics, and custom rules. Use go build when you need to verify that your code compiles and links against your dependencies. Use manual code review when you need to evaluate architecture decisions, error handling strategies, and long-term maintainability. Use go test when you need to verify that your code produces the expected output under real conditions.
Vet is the first line of defense. It is not the last.