skip to content

Run Control

Everything that changes what a go test invocation actually runs: the result cache and -count=1, build tags that hide slow suites, and TestMain wrapping a package. Suites go quiet here.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

23

In `go test` output, what does the `(cached)` marker mean, and how do you force a real run?

level: juniorimportance: must knowfreq 58%

answer

  1. the test binary never actually ran
  2. the marker sits where the duration goes
  3. failing runs are never reused
  4. one flag outside the cacheable set kills reuse

basics

~10 s

The (cached) marker means go test found a stored successful result for that package and reprinted its output instead of running the test binary. Only passing runs are stored; add -count=1 to force execution.

solid answer

~40 s

`ok example.com/pkg (cached)` means the go command recognised this exact package test run as one it has already done successfully, so it reprinted the earlier output instead of executing the test binary. The marker takes the place of the elapsed time, which is the giveaway that nothing ran. Only successful package results are cached — a failing run is never reused, so red output is always fresh. Caching happens in package list mode, for example `go test ./...` or `go test .`. To defeat it for one command, add `-count=1`: `-count` is not one of the flags the go command is willing to cache across, so the run is neither matched against the cache nor stored. To throw away every stored result on the machine instead, run `go clean -testcache`.

code

text · 8 lines
text
$ go test ./internal/rates/
ok  	example.com/billing/internal/rates	0.412s

$ go test ./internal/rates/
ok  	example.com/billing/internal/rates	(cached)

$ go test -count=1 ./internal/rates/
ok  	example.com/billing/internal/rates	0.409s

go deeper

for a junior

Be able to say in one sentence that the test binary did not run and that -count=1 makes it run. Recognise that the marker replaces the timing column in the summary line.

for a middle

Explain the mechanism behind the escape hatch: reuse requires every flag to come from a small cacheable set, and -count is not in it. Know that only passing package results are stored.

for a senior

Show that you treat a green (cached) line as a claim about observed inputs, not about the world. Say when you would clear results with go clean -testcache and when you would pin a job to -count=1 instead.

for a principal

Frame it as a trust boundary for the team: which suites are allowed to be cacheable, and which must always execute because they depend on state the go command cannot see. That policy is what keeps a repo-wide green meaningful.

## What `(cached)` actually reports When `go test` finishes a package successfully, the go command writes that package's test output and exit status into the build cache directory (`go env GOCACHE`) under a key derived from everything it believes the run depended on. The next time you ask for the same package with the same inputs, it does not compile-and-run anything: it looks the entry up, prints the stored output, and marks the summary line `(cached)` where the elapsed time normally goes. ``` ok example.com/billing/internal/rates 0.412s <- ran ok example.com/billing/internal/rates (cached) <- reprinted ``` So the marker is not "the tests ran quickly" and not "the tests were skipped". It is "the test binary was not executed; you are looking at a recording". With `-v`, the recorded verbose log is replayed too, which is why a cached run can print a wall of `--- PASS` lines instantly. ## Only successful runs are cached This is the property that makes the feature tolerable. A package that fails is re-run every single time, so you can never be looking at a stale failure. The risk runs entirely in the other direction: a stale *pass*, if something the go command could not observe changed underneath you. ## When caching applies at all The go command caches in package list mode — when you name packages, as in `go test ./...`, `go test .`, or `go test example.com/billing/...`. The local-directory form, plain `go test` with no package arguments, is documented as not caching, which is why some developers say they "never see (cached)" while a colleague sees it constantly. Results are stored per package: in a repo-wide run you routinely get a mix, a few packages executing and the rest replaying. ## Forcing a real run The conventional escape hatch is `-count=1`: ``` go test -count=1 ./internal/rates/ ``` It is worth knowing *why* this works, because the usual guess is wrong. `-count=1` is already the default number of runs, so it changes nothing about execution. It works because the go command only reuses a result when every flag on the command line comes from a small set it considers safe to cache across (`-run`, `-v`, `-short`, `-timeout`, `-cpu`, `-parallel`, `-failfast`, `-list`, `-fullpath`). `-count` is deliberately not in that set, so passing it makes the invocation uncacheable: the result is not matched against the cache and not stored either. Any other non-cacheable flag has the same effect; `-count=1` is simply the idiom because it is otherwise a no-op. The heavier hammer is `go clean -testcache`, which expires every stored test result in the build cache — all packages, all modules, everything that shares that `GOCACHE`. It leaves the compiled-package build cache alone, so your next build is still fast. `go clean -cache` is the one that wipes compiled artefacts too and makes the next build slow; reach for it only when you actually suspect the build cache. ## Why a package you did not touch can still be `(cached)` The entry is keyed on the test binary for *that* package, which is built from the package's own source plus all of its dependencies. Editing an unrelated package leaves that binary byte-identical, so the reuse is correct. Editing a package that the test imports rebuilds the binary and produces a different key, so it re-runs. That is the behaviour you want: the cache is a per-package memo, not a global timestamp. ## What to say in an interview Name the three facts: the binary did not run, only passes are cached, and `-count=1` disables reuse because `-count` is outside the cacheable flag set. Then add the honest caveat — the cache only knows about inputs the go command can see, so a test that talks to a service or reads a file outside its own source tree can go on printing `(cached)` after the thing it depends on has changed.

  • Can a failing test result ever come back as (cached)?
    No. The go command stores only successful package test results, so any failure you see was produced by a test binary that just ran. That asymmetry is deliberate: a stale red would be intolerable, while a stale green is at least bounded by what the go command can observe.
  • You edited a file in another package and that package's tests still print (cached) — is that a bug?
    No, if the edited package is not among that test binary's dependencies. The cache entry is keyed on the test binary, which is built from the package under test plus everything it imports. An unrelated edit leaves the binary identical, so replaying the result is correct.
  • A colleague says they never see (cached) locally. What is the likely reason?
    They probably run plain `go test` inside a package directory. Caching applies in package list mode — `go test .` or `go test ./...` — and the no-argument local-directory form is documented as not caching. Habitually passing extra non-cacheable flags, or a GOFLAGS setting containing -count=1, produces the same symptom.

saying these in an interview costs you the question

  • Thinks (cached) means the tests were skipped or had no test files
  • Believes -count=1 works by limiting the test to one execution
  • Claims failing results are cached too, so red output may be stale
  • Says the cache is per test function rather than per package
  • Reaches for go clean -cache when only test results need expiring
open as a page

How do you produce a `go test` coverage profile and view which lines were never executed?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Run go test -coverprofile=cover.out ./... to write a coverage profile file, then go tool cover -func=cover.out for per-function percentages, or go tool cover -html=cover.out to open a page where every uncovered source line is highlighted.

open as a page

What does go test -race do, and what happens to the run when it detects a race?

level: juniorimportance: must knowfreq 70%

basics

~20 s

go test -race compiles the tests with the race detector, which watches memory accesses as they run. A conflict prints a WARNING: DATA RACE report naming both accesses with their goroutine stacks, fails that test, and makes go test exit non-zero.

open as a page

What is func TestMain(m *testing.M), and when does go test call it instead of running tests?

level: juniorimportance: must knowfreq 55%

basics

~20 s

TestMain is an optional function a test package may declare. The test binary then calls it instead of running the tests directly, and the tests run only when it calls m.Run. It is the hook for package-wide setup and teardown.

open as a page

How do you put a Go integration test file behind a build tag so it runs only on demand?

level: middleimportance: must knowfreq 55%

basics

~20 s

Put //go:build integration on the first line of the _test.go file, followed by a blank line before the package clause. The file is then excluded from every ordinary go test run and compiled back in only by go test -tags=integration.

open as a page

What does the go test -short flag actually do, and how does a slow test opt out under it?

level: juniorimportance: should knowfreq 46%

basics

~10 s

The -short flag only sets a boolean that testing.Short() reports; it skips nothing on its own. Each slow test has to check testing.Short() itself and call t.Skip so the fast run passes over it.

open as a page

Which inputs decide whether `go test` may reuse a cached result for a package?

level: middleimportance: should knowfreq 44%

basics

~20 s

Three inputs: the test binary, built from the package and its dependencies; the command line, which must use only cacheable flags; and the files under the package's source tree and the environment variables the test read.

open as a page

Why does `go test -race` switch coverage counters to -covermode=atomic?

level: middleimportance: should knowfreq 38%

basics

~20 s

Coverage counters are ordinary shared variables. With set or count mode, two goroutines bumping the same counter is an unsynchronised write that the race detector reports and that loses increments. Atomic mode updates counters atomically, so go test picks it whenever -race is on.

open as a page

Why does `go test` report 0% coverage for a package that another package's tests exercise?

level: middleimportance: should knowfreq 42%

basics

~20 s

By default go test instruments only the package being tested, so calls into other packages are never counted. Pass -coverpkg with a package pattern, for example -coverpkg=./..., to instrument those packages too and credit cross-package execution.

open as a page

Why is a go test -race run so much slower and more memory-hungry than the same suite without it?

level: middleimportance: should knowfreq 55%

basics

~20 s

The -race build is a separate instrumented binary that calls into the race runtime on every memory access and keeps shadow state for the memory touched. Budget roughly 2-20x the run time and 5-10x the memory, plus a full rebuild of the dependency tree.

open as a page

In TestMain, what does the int returned by m.Run mean, and how must it reach go test?

level: middleimportance: should knowfreq 42%

basics

~20 s

m.Run returns the test binary's exit code: zero if every selected test passed, non-zero otherwise. TestMain either passes it to os.Exit or simply returns, and the generated test main exits with it. Hardcoding os.Exit(0) hides real failures.

open as a page

`go test ./...` keeps reporting (cached) for a package whose behaviour changed — how do you diagnose it?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Re-run that package with go test -count=1 and compare. If the outcome changes, the test depends on something the go command never recorded: a service, a database, the clock, or a file outside the package's source root.

open as a page

Your go test -race CI job now gets OOM-killed and blows its time budget. How do you get it green again without dropping the detector?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Measure per-package duration and peak memory first, then cut concurrency with go test -p and -parallel, shard the tree across jobs, shrink oversized fixtures and goroutine fan-out, and size the runner for the detector's five-to-ten-times footprint. Disabling the flag is the last resort.

open as a page

A Go tool's -tags=integration suite was green for weeks without running. How do you prove that?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Ask the go command which files it actually built: go list -f '{{.TestGoFiles}}' shows the tagged file missing, and go test prints a no-test-files line instead of running anything. The usual causes are a misspelled build tag or a pipeline that never passes -tags.

open as a page

A TestMain does defer lis.Close() then os.Exit(m.Run()), yet the listener leaks. Why, and what is the fix?

level: seniorimportance: should knowfreq 48%

basics

~20 s

os.Exit terminates the process immediately and runs no deferred function, so the deferred Close never fires. Capture the code from m.Run, run the teardown, and only then call os.Exit - or move the defers into a helper that returns the code.

open as a page

Your CI gate fails a PR when `go tool cover -func` reports under 80%. How do you decide what that number should measure?

level: principalimportance: should knowfreq 30%

basics

~20 s

Decide the measured set before the threshold: which packages -coverpkg instruments, whether end-to-end counters from GOCOVERDIR merge in, and whether the gate scores the repository total or only changed code. The number is only defensible once its scope is written down.

open as a page

How do you decide whether go test -race runs on every pull request, only nightly, or only over some packages?

level: principalimportance: should knowfreq 40%

basics

~20 s

Weigh where a race is cheapest to catch against what the instrumented run costs. A workable split runs the detector over the concurrent packages on every pull request and the whole tree nightly and before merge, with a named owner and a review date for every exclusion.

open as a page

How do you decide which Go test suites gate a merge and which sit behind a build tag?

level: principalimportance: should knowfreq 26%

basics

~20 s

Gate merges on suites whose failure the diff explains and a developer can diagnose in minutes; push the rest behind a build tag or a testing.Short guard. The decisive cost is that a tagged file is not even compiled by the default run.

open as a page

How does `go test -count=1` differ from `go clean -testcache` when defeating cached results?

level: middleimportance: nice to knowfreq 29%

basics

~20 s

-count=1 affects one invocation: it takes that command out of the cacheable flag set, so nothing is reused and nothing is stored. go clean -testcache is retroactive and machine-wide, expiring every test result already in the build cache.

open as a page

When is a _linux_test.go filename better than skipping the test with a runtime.GOOS check?

level: middleimportance: nice to knowfreq 27%

basics

~20 s

Use the _linux suffix when the file only compiles on Linux — the name alone keeps it out of every other build. Use a runtime.GOOS check with t.Skip when the file compiles everywhere and only the behaviour is Linux-specific.

open as a page

In TestMain, how do you define and read a custom test flag such as -dsn or -port?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Register the flag at package level in a _test.go file with flag.String or flag.Int, then call flag.Parse() inside TestMain before reading the value and before m.Run. go test passes flags it does not recognise through to the test binary.

open as a page

A binary built with `go build -cover` wrote nothing to GOCOVERDIR after an end-to-end run. Why?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

An instrumented binary writes its counter files when the process terminates normally, and only if GOCOVERDIR is set in its environment. A container or script that kills the server, or a crash, discards every counter — the run happened but nothing was recorded.

open as a page

What does the GORACE environment variable control, and when would you set halt_on_error or history_size?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

GORACE carries space-separated options read by the race runtime at process start. halt_on_error=1 stops at the first report instead of continuing; history_size, 0 to 7 with a default of 1, enlarges each goroutine's access history so the earlier access's stack can still be printed.

open as a page