The Green Suite and the Untested Branch
A checkout service ships with 84% coverage and a reputation for reliable tests. Then a refactor changes how the package reads CHECKOUT_TIMEOUT, and every deployment starts falling back to the default — because the only code that read that variable lived in an error branch, and no test ever forced a malformed or missing value. The suite stayed green through the whole refactor. The number had been measuring the wrong promise.
Coverage answers exactly one question: which statements ran during this test execution?
go test -cover ./... # one percentage per package
go test -coverprofile=cover.out ./checkout # record a profile
go tool cover -func=cover.out # per-function percentages
go tool cover -html=cover.out -o cover.html # annotated source in a browser
The profile is a list of file:startLine.startCol,endLine.endCol count, one entry per basic block. -func aggregates them, and sorting the output puts the conversation where it belongs:
go tool cover -func=cover.out | sort -k3 -t$'\t' | head -20
Reading the result is a hunt for functions near 0% and blocks inside functions near 100%. A function at 40% means tests never reached two of its three paths — usually the error handling, because happy-path tests return early. A function at 100% can still be wrong: coverage counts that a block executed, not that an assertion inspected its effect. A test that calls Apply(config) without checking the result covers every line and verifies nothing.
Three mechanics matter for real projects. -coverpkg widens attribution: by default, a test only counts statements in the package it tests, so exercising config through the checkout package does not credit config's lines. go test -coverpkg=./... ./checkout counts the whole module, which is how you see that the shared package everyone imports is covered by nobody. Combining profiles across packages requires writing distinct files (-coverprofile per invocation, then go tool cover -func on a merged set) or letting CI aggregate. Coverage follows -run: go test -run TestReload -cover measures only that test, so you can ask which branch of one function a specific scenario touches.
Treat the percentage as a smoke detector, not a scoreboard. The productive use is comparative: a pull request that adds 300 statements and three points of coverage added code with tests; one that adds code with no profile change did not. Teams that set an absolute target invite tests written to hit the number — asserting nothing, covering everything. Pair the profile with the question the number cannot answer: how fast is the code that did run?
Timing the Function That Serves the Traffic
A JSON API negotiates compact keys with a Normalize step on every request. Latency matters, and guessing between two implementations is not measurement. Go's benchmark functions look like tests but answer performance questions:
func BenchmarkNormalize(b *testing.B) {
input := strings.Repeat("aBc-Def/", 64)
for i := 0; i < b.N; i++ {
Normalize(input)
}
}
The name starts with Benchmark, the parameter is *testing.B, and the loop runs b.N times — but you never choose b.N. The testing package picks it: it starts small, times the loop, and increases N until the run lasts long enough to be meaningful. That calibration is why results are comparable across machines and why a hand-rolled for i := 0; i < 1000000; i++ is wrong: on a fast machine a million iterations finish before the clock resolution helps, and on a slow one they waste minutes.
go test -bench=Normalize ./pkg -run=^$
-run=^$ matches no tests, so the run skips straight to benchmarks — useful when the suite takes a minute and you want only numbers. The output:
BenchmarkNormalize-8 5120000 231.4 ns/op 64 B/op 1 allocs/op
Each column is a claim. 231.4 ns/op is wall time per iteration, dominated on this machine by the goroutine count in -8. 64 B/op and 1 allocs/op are the ones you can act on: one heap allocation per call means one GC-visible object per request, and at ten thousand requests per second that is ten thousand allocations per second the garbage collector must track. Ask for them with -benchmem on the command line or b.ReportAllocs() inside the function.
Allocations are where the compiler interferes. If the result of Normalize(input) is discarded, the compiler may eliminate the call entirely and the benchmark measures an empty loop:
var sink string // package-level so it cannot be optimized away
func BenchmarkNormalize(b *testing.B) {
input := strings.Repeat("aBc-Def/", 64)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
sink = Normalize(input)
}
}
Storing into a package-level sink forces the work to happen. If a benchmark reports near-zero time and zero allocations for work that clearly builds a string, suspect elimination before celebrating. The same discipline applies to what the benchmark builds: repeated append in a loop shows up immediately as B/op, and the fix is usually a strings.Builder with a pre-sized buffer rather than clever micro-optimizations.
Numbers Worth Quoting in a Pull Request
Two runs of the same benchmark on the same machine differ by 5–10%: the laptop is warm, a background process ran, the scheduler interfered. Quoting a single ns/op in review as proof that version A beats version B by 4% is quoting noise. The hygiene that makes benchmarks evidence follows four rules.
Move setup out of the loop. Anything that does not vary per iteration belongs before it, or is explicitly paused:
func BenchmarkParseConfig(b *testing.B) {
data, err := os.ReadFile("testdata/large.yaml")
if err != nil {
b.Fatal(err)
}
b.ResetTimer() // exclude file I/O already done above
for i := 0; i < b.N; i++ {
if _, err := ParseConfig(data); err != nil {
b.Fatal(err)
}
}
}
b.ResetTimer zeroes elapsed time and allocation counters after setup. When a single iteration itself has phases, b.StopTimer / b.StartTimer bracket the parts that should not count. Never let b.Fatal on setup run inside the loop — the failure cost becomes part of every iteration's time.
Repeat before comparing. One run per version is an anecdote. Run each version ten times and let a statistics tool compare:
go test -bench=Normalize -count=10 -benchmem ./pkg > old.txt
# switch branches
go test -bench=Normalize -count=10 -benchmem ./pkg > new.txt
benchstat old.txt new.txt
benchstat reports distributions and a significance verdict instead of two numbers. A change it calls insignificant is a change you do not write up.
Benchmark concurrency with RunParallel. For code behind a shared lock or a channel, per-goroutine timing lies:
func BenchmarkCounterParallel(b *testing.B) {
c := NewCounter()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Incr()
}
})
}
RunParallel splits b.N across GOMAXPROCS goroutines and reports aggregate throughput, which is the number that predicts behavior under worker pools and fan-out.
Benchmark the right function. Coverage tells you what tests touch; it does not tell you what the program spends time in. Find hot paths with a profile (pprof), then write benchmarks for those functions so a future optimization has a before-and-after. Benchmarks also do not prove correctness — they live beside the tests in _test.go but assert nothing. The suite's two instruments now answer different questions in one command: go test -cover -bench=. -benchmem ./... reports what ran and how fast it went, and the next topic asks what happens when the code under test talks to a database or a network it should not need.
Quiz
1. A package reports 100% coverage, yet a refactor that changed an environment-variable lookup broke production defaults. What does this reveal about coverage?
2. A colleague writes `for i := 0; i < 1000000; i++` inside a benchmark instead of looping over b.N. What is the consequence?
3. A benchmark of a string-building function reports 12 ns/op and 0 allocs/op even though the function clearly constructs a string. What is the most likely explanation?
4. Your optimization shows a 4% improvement in a single benchmark run. What is the responsible way to report it?
Flashcards
Question
What exactly does a coverage percentage count?
Click to reveal answer
Answer
Statements (basic blocks) executed during that particular test run, attributed to the package under test unless -coverpkg widens it. It does not count assertions, correctness, or paths a run never took — which is why a coverage number is a map of where tests looked, not a guarantee they checked anything.
Question
Who controls b.N in a benchmark, and why does that matter?
Click to reveal answer
Answer
The testing package does: it starts small, times the loop, and grows N until the run is long enough to be stable, then reports per-op cost. That calibration makes results comparable across machines; a hand-picked fixed loop reintroduces noise on fast hardware and wasted time on slow hardware.
Question
What do the B/op and allocs/op columns tell you that ns/op cannot?
Click to reveal answer
Answer
They measure heap pressure: bytes allocated and objects allocated per iteration. A function can be fast in ns/op while allocating per call, and at production request rates that becomes garbage-collector work. Report them with -benchmem or b.ReportAllocs().
Question
How do you exclude one-time setup from a benchmark's measurement?
Click to reveal answer
Answer
Do the setup first, then call b.ResetTimer() before the b.N loop — that zeroes elapsed time and allocation counters. Use b.StopTimer/b.StartTimer when setup occurs inside iterations. Allocation during setup also skews B/op, so report allocations after setup is done.
Question
Why is a single benchmark run insufficient for a performance claim?
Click to reveal answer
Answer
Machine state, thermal throttling, and the scheduler routinely move results by several percent. Run -count=10 per version and compare with benchstat; a change it cannot distinguish from noise is not one to quote in a pull request.
Revision Notes
Key Takeaways
- 1. Coverage profiles statements executed by this run — use -func output to find unexercised error paths, not to certify correctness
- 2. -coverpkg widens attribution to dependencies, exposing shared packages that no single test credits
- 3. Benchmarks rely on b.N chosen by the testing package; fixed-iteration loops destroy comparability
- 4. Store benchmark results in a package-level sink or the compiler eliminates the work and reports fiction
- 5. Quotes in review come from -count=10 plus benchstat, and allocs/op matters as much as ns/op for hot paths
Interview Tips
- • "How do you measure test coverage in Go?" — coverprofile plus `go tool cover -func`, then immediately say what the number does not measure: assertion quality and branches this run never forced
- • "Is 100% coverage a good goal?" — argue for changed-code coverage and uncovered-error-branch hunting; explain that a percentage target invites tests that call code without asserting
- • "Walk me through writing a benchmark" — b.N loop, sink variable, ReportAllocs, ResetTimer after setup, and why the framework owns N
- • "How would you prove an optimization worked?" — -count=10 on both versions, benchstat for significance, allocs/op before and after, and a profile showing the hotspot moved
Cheat Sheet
Coverage and Benchmarks
Command set:
| Goal | Command |
|---|---|
| Per-package percentage | go test -cover ./... |
| Profile + per-function table | go test -coverprofile=cover.out ./pkg && go tool cover -func=cover.out |
| Annotated source | go tool cover -html=cover.out |
| Count dependency code exercised through this package | go test -coverpkg=./... ./pkg |
| Run only benchmarks | go test -bench=. -run=^$ -benchmem ./pkg |
| Trustworthy A/B comparison | -count=10 per version, then benchstat old.txt new.txt |
| Parallelism-sensitive code | b.RunParallel(func(pb *testing.PB) { for pb.Next() { ... } }) |
Benchmark rules: loop for i := 0; i < b.N; i++; store the result in a package-level sink so the call is not eliminated; setup before the loop with b.ResetTimer(); b.ReportAllocs() for B/op and allocs/op; b.Fatal for setup failures, never inside the timed loop.
Reading output: BenchmarkX-8 5120000 231.4 ns/op 64 B/op 1 allocs/op — iterations run, time per op, bytes per op, allocations per op. Rising allocs/op in a hot path means GC pressure, not just memory.
Code template:
var sink Result
func BenchmarkProcess(b *testing.B) {
in := buildInput()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
sink = Process(in)
}
}
Continue Learning
Before You Start
- No prerequisites listed.