How go/analysis Quietly Became Go Tooling's Backbone
The framework behind go vet now powers Go 1.26's rewritten go fix — and your custom linters should build on it too.
A package that shipped its core design back in 2018 just spent a day on the Hacker News front page, and the timing isn't random. go/analysis — the Go team's modular static-analysis framework — has quietly become the single engine behind go vet, gopls diagnostics, and, as of Go 1.26, a completely rewritten go fix. If you write Go and you've ever wanted a custom lint rule enforced across your codebase, this is no longer one option among several. It's the standard, and the alternatives have mostly stopped mattering.
One interface, every driver
The framework's core idea is almost aggressively boring: an analysis is a value of type Analyzer — a name, a doc string, and a Run function that receives a Pass (one package's syntax trees and type information) and reports Diagnostics. That's it. The payoff is in what sits on either side of that interface.
On one side, analyzers compose. An analyzer declares prerequisites via Requires and consumes their results, so expensive work like building an AST traversal index happens once and gets shared. On the other side, drivers are interchangeable. The same analyzer runs under singlechecker as a standalone CLI, under multichecker bundled with others, under unitchecker inside go vet's build-integrated mode, inside gopls for as-you-type feedback, or inside Bazel's nogo. Write the check once; every tool in the ecosystem can host it.
The genuinely clever part is the "modular" in modular analysis. Whole-program analysis doesn't scale to large monorepos, so go/analysis borrows the separate-compilation trick: an analyzer can export serialized facts about a package — "this function is a Printf wrapper," say — and analyzers running later on downstream packages import those facts. The printf checker catches bad format strings in calls to your log.Fatalf-style wrappers across package boundaries without ever holding the whole program in memory. Facts ride the build cache, so incremental runs stay fast. The trade-off is real, though: you get per-package analysis with summaries, not global data-flow. Taint tracking across your whole service graph is out of scope, which is why tools like CodeQL and Semgrep still exist. For the 95% of checks that are local — API misuse, deprecated patterns, style invariants — the model is exactly right.
Go 1.26 closed the loop
For years the framework had a strange asymmetry: go vet was rebuilt on it (the vet redesign dates to 2017), but go fix — the tool that once migrated pre-Go 1 code — sat abandoned. Go 1.26 fixed that. The new go fix is a go/analysis driver whose analyzers don't just report diagnostics; they carry SuggestedFix payloads — concrete text edits — that the driver applies to your source. go vet and go fix are now near-identical programs that differ only in which analyzers they load and whether they print findings or rewrite files.
The launch suite is the "modernizers" that gopls users have been seeing as quick-fixes: minmax collapses if x < 0 { x = 0 } into min/max, rangeint turns three-clause loops into for range n, stringscut replaces strings.Index-plus-slicing with strings.Cut. The practical workflow after a toolchain upgrade is now:
go fix -diff ./... # preview
go fix ./... # apply; rerun — fixes unlock further fixes
Fixes are gated on your module's go directive, skipped in generated files, and syntactic conflicts are detected automatically. This is the piece Go tooling has been missing since gofmt proved that mechanical rewrites beat style debates: a sanctioned, safe channel for semantic rewrites at scale.
Writing your own is a weekend, not a quarter
Here's the developer-facing point: a bespoke company linter used to mean forking staticcheck or wrestling go/ast from scratch. Now it's roughly this:
var Analyzer = &analysis.Analyzer{
Name: "noctxbackground",
Doc: "forbid context.Background outside main and tests",
Run: func(pass *analysis.Pass) (any, error) {
// walk pass.Files, consult pass.TypesInfo,
// call pass.Reportf(pos, "...") on violations
return nil, nil
},
}
Wrap it in singlechecker.Main, and you have a binary you can run standalone or plug straight into the toolchain with go vet -vettool=$(which noctxbackground) — which means it inherits vet's build-cache integration for free. The analysistest package gives you table-driven tests where expected diagnostics live in // want comments next to the offending code. And because golangci-lint accepts go/analysis-based plugins and staticcheck exposes its checks as Analyzer values, your custom rule and the ecosystem's thousand existing ones run in one process, one config, one CI step. The package's import graph tells the adoption story: over 6,500 packages import go/analysis today.
Attach a SuggestedFix to your diagnostics and you get more than CI enforcement — gopls surfaces it as a quick-fix in every contributor's editor, and a go fix-style driver can apply it repo-wide. One implementation, three delivery channels.
The verdict, and the one caveat
This is the good kind of platform play: no plugin marketplace, no vendor, just a narrow interface that won by being the thing go vet itself is made of. The Go team has signaled where it goes next — letting library authors ship analyzers with their packages, so the maintainers of a SQL library can ship the injection check themselves and your tooling picks it up automatically. If that lands, Go gets something no mainstream ecosystem has: API-specific correctness rules that travel with the API.
The caveat is versioning. go/analysis lives in golang.org/x/tools, which is still v0 — the API is stable in practice but not by contract, and x/tools moves fast (v0.48.0 shipped in July 2026). Pin it, vendor your analyzer binaries in CI, and expect occasional churn in the surrounding packages like go/ast/inspector.
But don't let the v0 label fool you about maturity. If your team maintains grep-based lint scripts, a fork of somebody's linter, or a wiki page of "patterns we don't use" — that's technical debt now. The replacement is a hundred lines of Go against a stable core, and it runs everywhere your code does.
Sources & further reading
- analysis package - golang.org/x/tools/go/analysis — pkg.go.dev
- Using go fix to modernize Go code — go.dev
- x/tools/go/analysis/passes/modernize: publish — github.com
- How to create your own Go static analyzer? — pvs-studio.com
Priya covers AI frameworks, developer productivity tooling, and the startup ecosystem across South and Southeast Asia, bringing a researcher's rigour and a practitioner's empathy to every story. She is deeply sceptical of benchmarks and asks hard questions so her readers don't have to.
Discussion 0
No comments yet
Be the first to weigh in.