skip to content

Why can go test fail with a vet diagnostic before any of your tests run?

level: middleimportance: nice to knowfreq 35%

answer

  1. the test binary was never built
  2. go test does more than compile and run
  3. a curated always-worth-fixing list
  4. a wrong Test signature counts as a failure
  5. one flag turns the step off

basics

~20 s

go test runs a curated subset of go vet over the package before building anything. A diagnostic there aborts the run, so you see a vet message and no PASS or FAIL lines. The -vet=off flag skips the step.

solid answer

~50 s

`go test` does more than compile and run: before building the test binary it runs `go vet` over the package with a curated list of checks the Go team considers always worth fixing — among them `printf`, `tests` (malformed Test, Benchmark and Example signatures and names), `atomic`, `bool`, `errorsas`, `ifaceassert`, `nilfunc` and `stringintconv`. If any of them fires, the package fails immediately and you get a vet diagnostic with no test output at all, which is confusing the first time because the message often points at non-test code. The intended response is to fix the code; `go test -vet=off ./...` disables the step, and `-vet=` also accepts a comma-separated list of check names if you want a different set. Note that this is a deliberately small subset: the full `go vet` suite, which includes `copylocks`, `lostcancel` and `unusedresult`, is not run for you.

code

go · 9 lines
go
func TestParseConfig(t *testing.T) {
	// ...
}

// vet: wrong signature for TestReload
// Without the vet step this compiles and is silently never run.
func TestReload(t *testing.B) {
	// ...
}

go deeper

for a junior

Recognise the symptom: go test prints a vet message and no PASS or FAIL lines at all. Know that vet ran first and that the test binary was never built, let alone executed.

for a middle

Explain that go test runs a curated vet subset before building the tests, name a couple of the checks in it, and know that -vet=off disables the step while the full go vet suite is larger.

for a senior

This is the question you get pinged about: a contributor's build 'fails with no tests run'. Read the diagnostic, decide whether it is a real defect (it usually is), and explain why the subset is deliberately smaller than the full suite.

for a principal

Own the position on suppression: switching the step off buys a green build and gives up the only always-on correctness gate the toolchain provides for free. Decide instead where a full vet run belongs and who owns fixing what it turns up.

## The symptom Someone pushes a change, `go test ./...` comes back red, and the output contains no `--- FAIL`, no `PASS`, no test names — just a file, a line number and a sentence about a format verb or a function signature. It looks like the tooling is broken. It is not: the test binary was never built, because `go test` ran a vet pass first and that pass failed. ## What go test actually does Since Go 1.10, `go test` runs `go vet` on the package under test before compiling and running the tests. It does not run the whole shipped analyzer suite. It runs a curated list, chosen because those checks are cheap and have essentially no false positives — if one fires, the code is almost certainly wrong. The set includes, among others: - `printf` — format verbs that do not match their arguments, wrong argument counts, directives in `Println`. - `tests` — malformed test declarations: a `Test` function with the wrong signature, a `Benchmark` taking `*testing.T`, an `Example` whose name refers to a symbol that does not exist or whose `Output:` comment is malformed. - `atomic` — an assignment like `x = atomic.AddInt64(&x, 1)`, which is not atomic at all. - `bool` — suspicious boolean expressions such as a condition repeated on both sides of `&&`. - `errorsas` — a second argument to `errors.As` that is not a pointer to a type implementing `error`. - `ifaceassert`, `nilfunc`, `stringintconv` — impossible interface assertions, comparing a function to nil rather than calling it, and `string(intValue)` conversions that produce a rune rather than digits. A diagnostic from any of them is a build-level failure for that package. Nothing is run. ## Why the `tests` analyzer is in there This one deserves attention because of how it fails without vet. The testing framework discovers tests by name and signature: a function is a test only if it is named `TestXxx` and takes exactly `*testing.T`. Write `func TestReload(t *testing.B)` and it is a perfectly legal function that the test runner simply ignores. Before the vet step existed, the result was a test that silently never ran — green build, zero coverage of that path. The check turns a silent skip into a loud failure, which is the whole point. ## Controlling it - `go test -vet=off ./...` skips the vet step entirely. - `go test -vet=printf,tests ./...` runs exactly the named checks instead of the default list. - Leaving the flag out runs the curated default. `-vet=off` is occasionally the right call — bisecting an unrelated failure, or working inside a tree that is mid-migration — but it is a poor default. The checks in the list were picked precisely because a hit is nearly always a genuine bug, and switching them off removes the only always-on correctness gate the toolchain gives you for nothing. ## Full vet is still bigger The important asymmetry: `go vet ./...` runs the whole shipped suite, and it will report things a green `go test` never mentioned — `copylocks` on a struct with a `sync.Mutex` copied by value, `lostcancel` on a `context.CancelFunc` that is never called, `unusedresult` on a discarded `fmt.Sprintf`. So "the tests pass" does not mean "vet is clean". Reading the output of a full `go vet ./...` line by line finds a different, and often more interesting, class of defect than the test-time subset does. ## And `go build` runs none of it Worth saying explicitly, because it is a common assumption: `go build` does not run vet. A package that compiles has had none of these checks applied. Only `go vet` and the pre-test step inside `go test` run analyzers.

  • Which mistakes in the test files themselves does that step catch?
    The `tests` analyzer covers malformed test declarations: a `Test` function whose parameter is not `*testing.T`, a `Benchmark` taking the wrong type, an `Example` whose name refers to a symbol that does not exist or whose `Output:` comment is malformed. Without it these compile fine and are simply never discovered by the runner, so you get a green build with a path that was never exercised.
  • Why might go vet ./... report problems that a passing go test never mentioned?
    Because `go test` runs only a curated subset. Checks such as `copylocks` (a `sync.Mutex` copied by value), `lostcancel` (a `context.CancelFunc` that is never called on some path) and `unusedresult` (a discarded `fmt.Sprintf`) are in the full suite but not in the test-time list. A green test run is not evidence that vet is clean.
  • When is -vet=off a reasonable thing to reach for?
    Rarely, and temporarily: bisecting a failure that has nothing to do with the diagnostic, or working in a tree mid-migration where you will fix the reports in a separate change. The checks were chosen for near-zero false positives, so a hit almost always means real broken code — turning the step off to get green is how a genuine bug ships.

saying these in an interview costs you the question

  • Thinks a vet diagnostic means a test failed
  • Says go build runs vet as well
  • Believes go test runs the full vet suite
  • Reaches for -vet=off as the normal fix
  • Assumes only _test.go files are analyzed