What does go vet's printf analyzer catch in fmt.Printf calls that the compiler cannot?
answer
- the compiler sees a string and ...any
- a constant format can be parsed statically
- verb versus the argument's static type
- helpers that forward format and args are inferred
- the runtime alternative is a %! marker
basics
~20 sIt compares a constant format string against the arguments actually passed: a verb that does not match the argument's type, too few or too many arguments, and formatting directives handed to fmt.Println. The compiler cannot, because Printf takes ...any.
solid answer
~50 s`fmt.Printf` is declared as `Printf(format string, a ...any)`, so as far as the compiler is concerned `fmt.Printf("%d\n", "hello")` is perfectly well typed — a string and one interface value. The mistake only shows up at runtime, where `fmt` formats by reflection and prints `%!d(string=hello)` instead of failing. go vet's printf analyzer closes that gap statically: when the format string is a constant it parses the verbs and checks each against the static type of the matching argument, reports missing or extra arguments, flags `%w` used outside error wrapping, and flags a formatting directive passed to `fmt.Println`, which has no format string at all. It also infers printf wrappers, so a house logging helper that forwards its format and args to a `fmt` function gets its own call sites checked. Since Go 1.24 it additionally reports a non-constant format string in a call with no other arguments, the classic `fmt.Errorf(err.Error())`.
code
go · 8 linesfunc logHit(path string, code int) {
// %s against an int
fmt.Printf("served %s with %s\n", path, code)
// one verb, two arguments
fmt.Printf("served %s\n", path, code)
// a directive in a Println call
fmt.Println("served %s", path)
}go deeper
Know that go vet ships with the toolchain and that its best-known check compares fmt.Printf verbs against the arguments you passed. Be able to name one mistake it catches, such as %d given a string.
Explain why the compiler cannot do this — Printf's signature is (string, ...any) — and how the analyzer works instead: parse the constant format, match each verb against the argument's static type, and report count mismatches.
Talk about the payoff in a repository with many occasional contributors: a mismatched verb is a defect every test passes over that surfaces as a garbled log line during an incident. Mention wrapper inference so the house logging helper is covered too.
Weigh what belongs in an always-on, near-zero-false-positive check versus an optional one a team opts into, and plan for the day a newly added check starts reporting existing code across the whole repository.
## Why the compiler is blind here `fmt.Printf` has the signature `func Printf(format string, a ...any) (n int, err error)`. The format string is an ordinary `string` parameter and the arguments are a variadic `...any`, so **every** call type-checks: any value satisfies `any`, and the compiler has no idea that `"%d"` is supposed to correspond to the first element of `a`. The relationship between the verbs and the arguments is a convention encoded in a string, and strings are not part of the type system. What happens instead is that `fmt` inspects the arguments at run time with reflection. It never returns an error you would notice; it embeds a marker in the output. `fmt.Printf("%d\n", "hello")` prints `%!d(string=hello)`; a missing argument prints `%!d(MISSING)`; an extra one prints `%!(EXTRA string=hello)`. Those markers land in a log line, which is exactly where nobody reads them until an incident. ## What the analyzer does The `printf` analyzer, shipped with the toolchain and run by `go vet`, is the oldest and most valuable vet check. For every call to a function it knows to be printf-like: - **If the format string is a constant** it parses the verbs and matches them, positionally, against the static types of the remaining arguments. `%d` against a `string`, `%s` against an `int`, `%s` against a type with no `String` method and no string-ish representation — all reported. - **Argument count** is checked: `fmt.Printf("%s\n", path, code)` has one verb and two arguments, and `fmt.Printf("%s %s\n", path)` has two verbs and one. - **`%w`** is the error-wrapping verb and is only meaningful in `fmt.Errorf`; using it in `Printf` or `Sprintf` is reported, as is passing a non-error to it. - **Print-family calls that contain directives**: `fmt.Println("count: %d", n)` is reported as a possible Printf formatting directive in a Println call, because `Println` has no format string and will print the literal `%d` followed by the value. ## Wrapper inference Most real codebases do not call `fmt.Printf` directly; they call something like `func (l *Logger) Infof(format string, args ...any)`. The analyzer handles this by inference: if a function ends in a `string` parameter followed by `...any`, and its body passes both straight through to a known printf-like function, it is itself treated as printf-like, and its own call sites are checked with the same rules. That is what makes the check useful in a repository with a house logging helper rather than only at direct `fmt` calls. ## The non-constant format string A format string that is not a constant cannot be parsed at analysis time, so historically vet said nothing. Go 1.24 added one important case back: a call of the form `fmt.Printf(s)` or `fmt.Errorf(s)`, where `s` is non-constant and there are no further arguments, is now reported. The reason is that this shape is almost always a bug — `fmt.Errorf(err.Error())` treats whatever the error text happens to contain as a format string, so an error whose message includes a stray `%s` produces `%!s(MISSING)` inside the new error. The fix is `fmt.Errorf("%s", err)` or, better, `fmt.Errorf("reading config: %w", err)`. ## Where it runs `go vet ./...` runs the printf analyzer along with the rest of the shipped suite. `go test` also runs it, as part of the small curated subset it applies before building your tests, so a mismatched verb anywhere in the package can fail a test run outright. `go build` does **not** run vet — compiling successfully tells you nothing about your format strings. ## Why it matters in practice This is a bug class that tests pass straight over. Nothing panics, nothing returns an error, no assertion fails; the only symptom is a log line with a `%!` marker in it, discovered later by whoever is trying to work out what a service was doing at the time. In a repository with dozens of occasional contributors and no shared house style beyond the tools, an always-on, near-zero-false-positive check that reads the format string for you is worth far more than a style rule.
- What happens at run time if the mismatch is never caught?Nothing fails. `fmt` formats through reflection and writes a marker into the output instead: `%!d(string=hello)` for a wrong verb, `%!d(MISSING)` for an absent argument, `%!(EXTRA int=404)` for a spare one. The program keeps running and the damage is a log line that is wrong exactly when someone needs to read it.
- How does the analyzer know that a house logging helper is printf-like?It infers wrappers. A function whose parameters end in a format `string` followed by `...any`, and whose body forwards both directly to a function already known to be printf-like, is treated as printf-like itself. Its own call sites are then checked with the same verb and argument rules, which is what makes the check cover real codebases rather than only direct `fmt` calls.
- What does go vet report for fmt.Println("id: %d", id)?It reports that the `Println` call contains a possible `Printf` formatting directive. `Println` has no format string: it prints its operands separated by spaces, so the output is the literal text `id: %d` followed by the number. The fix is `fmt.Printf("id: %d\n", id)`.
saying these in an interview costs you the question
- Says the compiler type-checks printf verbs
- Thinks a wrong verb panics at run time
- Claims vet can check a format string built at run time
- Believes only direct fmt calls are analyzed
- Assumes %w works in fmt.Printf
- Thinks go build runs vet for you