Dev & Code 21 h agoAdd to bookmarks

Under the hood of `go vet`, `staticcheck`, and friends: the Go team maintains an official framework that allows you to write a linter in a few dozen lines.
You inherit a 200,000-line Go project. For six months, a code review pattern that causes goroutine leaks has been slipping through: a context.WithTimeout created without defer cancel(). No one sees it when reading. go vet doesn't report it. Adding a rule to the in-house linter would take about a week in another language.
In Go, you write an Analyzer in an evening. Because the language team has long maintained a quasi-discreet but fundamental package: golang.org/x/tools/go/analysis.
The go/analysis package ("Go Analysis Framework") is not new - it is published and documented at pkg.go.dev/golang.org/x/tools/go/analysis - but it regularly resurfaces on Hacker News (last resurgence on 2026-07-26) because many Go developers still don't know about it. Yet it is the infrastructure on which the language's static analysis ecosystem runs: go vet relies on it, staticcheck (Dominik Honnef) uses it, gopls (the official language server) connects its diagnostics to it, and the vast majority of modern CI linters - golangci-lint in the lead - are just collections of Analyzers stacked together.
The framework's bet is simple: one analyzer = one struct. This struct declares its name, its documentation, its dependencies on other analyzers, and a Run function that receives a *analysis.Pass object (the parsed code, the type checker, the files, the comments) and reports its diagnostics via pass.Report. That's all.
Three ideas drive the architecture:
Modularity through composition. An analyzer can declare Requires: []*analysis.Analyzer{inspect.Analyzer}: the framework resolves the dependency graph and injects the results of the prerequisite analyzers into the Pass.ResultOf. Result: your business analyzer does not re-parse the files, it consumes the result of inspect (which, in turn, does the typed AST walk once for the entire chain).
One "driver", multiple bootstrappings. You write your Analyzer. singlechecker.Main(myAnalyzer) makes it a standalone CLI binary, multichecker.Main(a1, a2, a3) makes it a multi-check binary, unitchecker.Main(...) connects it to go vet -vettool=.... Zero glue code: the same Analyzer code runs locally, in CI, in the IDE via gopls, and in golangci-lint.
Typed automatic fix. A diagnostic can carry SuggestedFixes: textual replacements anchored on token.Pos positions. gopls applies them with one click in the editor. This is what allows, for example, staticcheck to suggest "replace errors.Is(err, sql.ErrNoRows) with a more idiomatic check" and the IDE to make the substitution properly.
To give an order of magnitude: singlechecker - the function that turns an *Analyzer into a complete binary - fits in one file. The minimal skeleton of a publishable Go linter is roughly 40 lines, doc and tests included. Compare that to writing an ESLint plugin from scratch: it's not the same scale.
Three uses we see in real projects:
net/http, you have to go through our tracing wrapper." Thirty lines, an Analyzer that inspects the *ast.ImportSpec, a test with analysistest.Run on a fixture file, and the rule is connected to CI the next day.Analyzer with SuggestedFix turns the operation into go vet -vettool=./monfix -fix ./.... Zero humans in the loop.ctrlflow (control flow graph), buildssa (SSA representation of Go code) - on which non-trivial dataflow analyses can be connected. Detecting leaks of unclosed sql.Rows, for example, is done seriously from the SSA, not from the raw AST.Two blind spots to keep in mind:
Analyzer sees one package at a time. Inter-package analyses (whole-program) require additional work - it's possible with "facts" (analysis.Fact), which allow an analyzer to export facts about the symbols of a package so that these facts are reused when another package imports it, but it's a more complex level to write.Analyzer into go vet via runtime config: you compile it into a binary. It's a choice (Go doesn't like dynamic plugins), not an oversight.go/analysis is the official API for everything related to static analysis in Go, from the in-house linter to automatic code migration.Run(*Pass). The rest (CLI, go vet integration, gopls integration, CI) is offered to you.sed, that evening is well invested.Article produced by artificial intelligence, reviewed under human editorial control.