In golang.org/x/tools/go/analysis, what is an *analysis.Analyzer made of, and how does its Run function report a finding?
answer
- a struct value, not an interface
- the driver hands you one package
- Name and Doc are the user-facing surface
- findings go out sideways, not through return
- the error return means the tool broke
basics
~20 sAn analysis.Analyzer is a struct: Name, Doc, Requires and a Run function. Run is called once per package and calls pass.Reportf with a source position for each finding. A non-nil error from Run means the analyzer itself failed.
solid answer
~50 sA custom `go vet` check is one exported variable of type `*analysis.Analyzer`, not an interface implementation. `Name` becomes the check's flag name and prefixes its diagnostics, `Doc` is the help text a reader sees when the check fires, `Requires` lists analyzers whose results this one needs, and `Run func(*analysis.Pass) (any, error)` is the check itself. The driver parses and type-checks one package, then calls `Run` once for that package with an `analysis.Pass` carrying `Files`, `Fset`, `Pkg`, `TypesInfo` and `ResultOf`. Findings never come back through the return value: you call `pass.Reportf(pos, format, args...)` with a `token.Pos` from that pass's `Fset`, or `pass.Report` with a full `analysis.Diagnostic` when you want an end position or a suggested fix. The error return is reserved for the analyzer breaking, and the result value is what other analyzers listed in their `Requires` will read.
code
go · 19 linesvar Analyzer = &analysis.Analyzer{
Name: "nobody",
Doc: "reports functions declared without a body",
Run: run,
}
func run(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok || fn.Body != nil {
continue
}
pass.Reportf(fn.Pos(), "%s is declared without a body", fn.Name.Name)
}
}
// nil error: the analyzer worked. Findings already went to pass.Reportf.
return nil, nil
}go deeper
Recall that a custom go vet check is a struct value with a name, a doc string and a Run function, and that Run is given one already-parsed package at a time.
Be ready to explain the split between reporting a diagnostic and returning an error, what an analysis.Pass carries, and why the driver rather than your code decides ordering and caching.
Show that you keep Run deterministic, allocation-light and free of global state, and that you write diagnostic messages a stranger can act on without reading your source.
Own what the Doc string promises: it is the contract every team reads the first time your check blocks them, and a vague one turns a correct check into a support queue.
## A check is a value, not an interface `golang.org/x/tools/go/analysis` is the framework the Go toolchain's own vet checks and every custom vet pass are written against. The unit is a single exported package-level variable: ```go var Analyzer = &analysis.Analyzer{ ... } ``` There is no interface to implement and no base type to embed. That matters more than it looks: because a check is an ordinary value, drivers can compose checks from many repositories into one binary, build a dependency graph over them, and decide scheduling and caching without the check knowing anything about it. ## The fields - **`Name`** — an identifier-shaped name (`auditlog`, `noctxtodo`). The driver turns it into a flag so a user can enable or disable this check alone, and prefixes the check's diagnostics with it. - **`Doc`** — documentation. The first line is a one-line summary shown in the tool's help; the rest should explain what the check finds *and what to write instead*. When a mandatory check fires in someone else's build at 5pm, this string is the entire support experience. - **`Run func(*analysis.Pass) (any, error)`** — the check. - **`Requires []*analysis.Analyzer`** — other analyzers that must run first on the same package; their outputs arrive in `pass.ResultOf`. - **`ResultType`** — a `reflect.Type` describing what `Run` returns for downstream analyzers; leave it unset when nothing consumes you. - **`FactTypes`** — declares the serializable facts this analyzer exports about declarations, which is how information crosses package boundaries. - **`Flags`** — a `flag.FlagSet` for the check's own options; the driver namespaces them under the check's name. - **`RunDespiteErrors`** — allow the driver to run the check on packages that failed to parse or type-check. Off by default. ## One package per Run call The driver does the parsing and type-checking and hands `Run` a single package through `*analysis.Pass`: - `pass.Files` — the parsed `*ast.File` for each Go file in the package. - `pass.Fset` — the `*token.FileSet` those positions belong to. - `pass.Pkg` — the `*types.Package`. - `pass.TypesInfo` — the `*types.Info` resolving identifiers and expressions. - `pass.ResultOf` — a map from each analyzer in `Requires` to its result for this package. - `pass.OtherFiles` / `pass.IgnoredFiles` and `pass.ReadFile` — non-Go files and files excluded by build constraints, readable through the driver so it can account for them. There is no whole-program view. A check that wants to know something about a function declared in another package has to have exported a fact about it when that package was analysed. ## Reporting ```go pass.Reportf(call.Lparen, "result of %s is never used", name) ``` The first argument is a `token.Pos` from *this* pass's `Fset`; the driver converts it to `file:line:col`. Positions from any other `FileSet` produce nonsense locations, which is why you take positions off the nodes you were given rather than re-parsing anything yourself. `pass.ReportRangef` takes a node so the diagnostic covers a span, and `pass.Report(analysis.Diagnostic{...})` gives you the full struct: `Pos`, `End`, `Category`, `Message`, `URL`, `Related` and `SuggestedFixes`. A `analysis.SuggestedFix` is a message plus `analysis.TextEdit` values (`Pos`, `End`, `NewText`); drivers that support fixes can apply them, and plain `go vet` prints only the message. Message style follows the toolchain's: a short lowercase phrase, no trailing period, naming the thing that is wrong. The reader is looking at one line of output in a failing build. ## What the return values mean This is the distinction interviewers actually probe. `Run` returns `(any, error)`: - The **error** means *the analyzer could not do its job* — an internal invariant broken, input it cannot interpret. It is a tool failure and drivers surface it as one. Reporting a problem in the user's code through the error return is a category mistake: it aborts the run instead of producing a diagnostic. - The **any** is the analyzer's result for downstream consumers, matching `ResultType`. Most checks return `nil, nil`. ## Composition through Requires `Requires` makes the checks a DAG. The driver runs each dependency once per package and puts the value in `pass.ResultOf[dep]`, which you type-assert. The near-universal example is `inspect.Analyzer` from `golang.org/x/tools/go/analysis/passes/inspect`: its result is a shared `*inspector.Inspector` so that twenty checks in one binary traverse the package's syntax once instead of twenty times. ## Rules Run has to respect Run must be deterministic and free of global state: many packages are analysed in one process, possibly concurrently, and results are cached. Do not mutate the AST — other analyzers are handed the same trees. Do not print to stdout or exit; report and return. Keep it cheap, because it runs on every package of every build for everyone who has the check enabled.
- What is the Requires field for, and how does an analyzer read what a dependency produced?`Requires` lists other analyzers that must run first on the same package. The driver runs each once and stores its value in `pass.ResultOf[dep]`, which you type-assert to the dependency's `ResultType`. The standard case is `inspect.Analyzer`, whose result is a shared `*inspector.Inspector`, so many checks in one binary walk the package's syntax a single time instead of once each.
- When should Run actually return a non-nil error?Only when the analyzer itself cannot proceed: an internal invariant is violated, or input it must read is unusable. A defect in the code being analysed is never an error — it is a diagnostic. Drivers treat an error as a tool failure and report it as such, so using it for findings both loses the position information and stops the run.
- How does a check offer a fix rather than only describing one?Build an `analysis.Diagnostic` and attach `SuggestedFixes`: each is a message plus `TextEdit` values with `Pos`, `End` and `NewText`, then pass it to `pass.Report`. Edits must be non-overlapping and should be minimal. Drivers that support fixes can apply them mechanically; plain `go vet` prints only the message, so the diagnostic still has to read well on its own.
The Analyzer is a cartridge and the driver is the console: you supply a name, a manual and a Run function, and the driver decides what gets loaded, in what order, and what is cached.
saying these in an interview costs you the question
- Returning discovered problems as Run's error value
- Thinking Analyzer is an interface you implement
- Assuming Run can see the whole program at once
- Leaving Doc empty so the check has no help text
- Reporting a position taken from a different token.FileSet
- Mutating the AST that other analyzers also receive