How to Use staticcheck for Advanced Static Analysis

Install staticcheck via go install and run staticcheck ./... to analyze your Go code for advanced static analysis issues.

The compiler is not a mind reader

You write a Go service. It compiles. The tests pass. You deploy it to staging. Three hours later, the logs fill up with panics from a nil pointer dereference you missed, or the CPU spikes because a loop allocates a new slice on every iteration. The Go compiler is strict about types, but it does not read your intent. It will not tell you that you are ignoring an error that will fail in production, or that you are using a deprecated function, or that your channel send will block forever. You need a tool that reads your code like a senior engineer would.

What static analysis actually does

Static analysis tools inspect your source code without running it. They parse the text into an abstract syntax tree, trace how data flows through variables and function calls, and match patterns against a database of known issues. go vet ships with the toolchain and catches obvious mistakes like format string mismatches or unreachable code. staticcheck goes further. Think of go vet as a spellchecker that flags typos and missing punctuation. staticcheck is the technical editor who knows your entire codebase, remembers every style guide rule, and spots logical gaps before they reach production. It runs locally, finishes in milliseconds, and gives you actionable fixes.

Static analysis catches patterns. Tests catch behavior.

A minimal example

Here is a small program that compiles cleanly but hides three common mistakes that staticcheck will flag immediately.

package main

import (
	"fmt"
	"io/ioutil" // deprecated since Go 1.16, triggers SA1019
)

func main() {
	// SA4006: result of expression not used
	fmt.Println("starting")

	// SA1029: ignoring error from ioutil.ReadFile
	ioutil.ReadFile("config.txt")

	// ST1000: unreachable code after return
	return
	fmt.Println("done")
}

Run the tool from your terminal with a single command. The output points to the exact line, gives a short code, and explains the issue in plain English.

staticcheck ./...

The tool scans the file, builds an internal representation of the code, and compares it against hundreds of checks. It does not execute the program. It reasons about control flow, type inference, and common anti-patterns. When it finds a mismatch, it prints a diagnostic. You fix the code, run the tool again, and the warning disappears.

How it reads your code

staticcheck does not just grep for strings. It uses the Go compiler's parser to build an AST, then converts that tree into Static Single Assignment form. SSA is a representation where every variable is assigned exactly once. This makes data flow analysis trivial. The tool can track where a value comes from, where it goes, and whether it is ever checked.

The analysis happens in passes. The first pass checks syntax and basic type rules. The second pass traces control flow to find unreachable branches or missing error checks. The third pass matches against a curated database of checks grouped by prefix. SA stands for static analysis. ST covers style and naming. QC handles quality and correctness. EX flags experimental checks. Each check is independent, so you can enable or disable them without breaking the rest.

The tool caches results between runs. It only reanalyzes files that changed. This keeps the feedback loop tight. You can run it on save, on pre-commit, or in CI. The output format matches standard linter expectations, so your editor highlights warnings inline.

Configure once. Run on every save.

Realistic project setup

In a real project, you rarely run the tool with zero configuration. You want to suppress checks that do not apply to your codebase, or you want to enable stricter rules for critical paths. staticcheck reads a configuration file named .staticcheck.conf in your project root, or you can pass flags directly.

Here is a typical configuration that enables all checks except a few noisy ones, and sets the output format to JSON for CI integration.

{
  "checks": [
    "all",
    "-ST1000",
    "-SA4006"
  ],
  "exit-code": 1,
  "output-format": "json"
}

The checks array uses all as a base, then subtracts specific codes with a leading dash. The exit-code field tells the tool to return a non-zero status when warnings are found, which fails CI pipelines automatically. The output-format field switches to machine-readable JSON so your build system can parse it.

You can also suppress warnings inline when you know a false positive is happening. The community convention is to use //nolint:staticcheck or //nolint:SA1019 on the line above the code. Add a comment explaining why. Do not blanket suppress entire files. If you find yourself suppressing the same check repeatedly, adjust the configuration instead.

Suppress sparingly. Document why.

Pitfalls and limits

Static analysis is powerful, but it is not omniscient. The tool cannot know runtime values. It cannot predict what a third-party library will return. It reasons about code paths, not execution environments. This means false positives happen. You might see a warning about a nil pointer dereference that is impossible in practice because a database query always returns a valid row. Or you might get flagged for ignoring an error that your team has decided is safe to drop.

When the tool flags something, verify it against your business logic. If the warning is correct, fix the code. If the warning is a false positive, suppress it with a targeted //nolint comment and move on. Do not let the linter dictate your architecture. Let it catch the low-hanging fruit.

The Go compiler will still reject invalid code. If you reference a missing variable, the compiler rejects this with undefined: x. If you pass the wrong type to a function, you get cannot use x (type int) as string in argument. staticcheck runs after parsing, so it never blocks compilation. It only warns. Treat warnings as suggestions, not hard errors. You can ignore them, but you should not.

Goroutine leaks are another area where static analysis has limits. The tool can prove a channel never closes if the code path is obvious. It cannot prove that a background worker will eventually exit if the cancellation logic lives in a different package. Always design your concurrency with explicit cancellation paths. The worst goroutine bug is the one that never logs.

Trust the linter. Verify the fix.

When to reach for which tool

Use go vet when you need a zero-dependency check that ships with the toolchain and catches basic format string mismatches or unreachable code. Use staticcheck when you want deeper data-flow analysis, pattern matching against deprecated APIs, and consistent style enforcement across a large codebase. Use the race detector when you suspect concurrent memory access violations that static analysis cannot prove. Use manual code review when you need to evaluate architectural decisions, business logic correctness, or naming conventions that tools cannot quantify.

Static analysis catches patterns. Tests catch behavior.

Where to go next