skip to content

Coverage Profiles

A green suite says nothing about what it never touched. go test writes a coverage profile that go tool cover turns into an annotated source view or a per-function table.

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

questions

5

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

level: juniorimportance: must knowfreq 62%

answer

  1. a number is not enough
  2. one flag writes a file
  3. two ways to read that file
  4. -func for a table, -html for colour
  5. statements executed at least once

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.

solid answer

~40 s

`go test -cover` prints a percentage per package, but to see *what* is uncovered you need the profile: `go test -coverprofile=cover.out ./...`. That writes a text file listing every instrumented block as `file:startLine.col,endLine.col numStatements count`. Two commands read it back: `go tool cover -func=cover.out` prints a per-function table plus a `total:` line, and `go tool cover -html=cover.out` renders the source with covered blocks in green and uncovered ones in red (`-o report.html` writes the file instead of opening a browser). The number is statement coverage: the share of instrumented statements that executed at least once during the run. It says nothing about whether the test asserted anything about them.

code

text · 8 lines
text
$ go test -coverprofile=cover.out ./...
ok      example.com/m/store     0.412s  coverage: 78.3% of statements

$ go tool cover -func=cover.out
example.com/m/store/store.go:14:        Get     90.0%
total:                          (statements)    78.3%

$ go tool cover -html=cover.out -o cover.html

go deeper

for a junior

Be ready to type the two commands from memory: go test -coverprofile=cover.out ./... and then go tool cover -html=cover.out. Know that the number is statements executed, not lines and not branches.

for a middle

Explain what the profile file contains — one line per basic block with a statement count and a counter — and why -coverprofile implies -cover while plain -cover writes nothing to disk.

for a senior

Show how you wire this into CI: -o for the HTML artifact, -func for the grep-able total, and awareness that an instrumented build is slower and covers only the package under test by default.

for a principal

Be ready to say what the number is allowed to decide in your organisation and what it is not, and to defend the scope you chose to measure rather than the digit itself.

## What the Go toolchain actually measures Go's coverage is **statement coverage**, computed by source rewriting. When you pass `-cover`, the toolchain makes an instrumented copy of each package under test: it splits every function into basic blocks (a straight run of statements with no branch in or out) and inserts a counter update at the top of each block. Compiling and running that copy tells you which blocks executed. The percentage `go test` prints is `covered statements / total instrumented statements`. ## The three commands **1. Produce a number.** ``` go test -cover ./... ``` Each package line gains `coverage: 78.3% of statements`. This is enough for a quick check but tells you nothing about which lines are missing. **2. Produce a profile.** ``` go test -coverprofile=cover.out ./... ``` `-coverprofile` implies `-cover`. It writes a text file whose first line is the mode header and whose remaining lines are one block each: ``` mode: set example.com/m/store/store.go:14.28,17.2 2 1 example.com/m/store/store.go:19.31,21.16 2 0 ``` The fields are: the import path plus file, the block's start `line.column` and end `line.column`, the number of statements in the block, and the counter. In `set` mode that counter is `1` if the block ran and `0` if it did not; the second line above is a block that never executed. **3. Read the profile back.** ``` go tool cover -func=cover.out go tool cover -html=cover.out -o cover.html ``` `-func` prints one row per function with its percentage and a final `total:` row — the form CI logs usually capture. `-html` renders each source file with covered blocks shaded green, uncovered blocks red, and untracked text grey; without `-o` it writes a temporary file and opens your browser, which is why CI always passes `-o`. ## Things that surprise people the first time - **The profile is per-run, not cumulative.** One `go test -coverprofile=...` invocation over many packages writes one file. `go tool cover` has no merge subcommand, and the text format carries a single `mode:` header, so you cannot simply concatenate two profiles from two runs; run one command over all the packages you care about instead. - **By default only the package under test is instrumented.** A test in package `a` that calls into package `b` does not credit `b` with anything unless you pass `-coverpkg`. - **Coverage measures execution, not assertion.** A test that calls a function and checks nothing still turns the block green. That is a property of the mechanism, not a flaw in the report. - **Instrumentation costs.** The instrumented build is a different build, so `-cover` runs are slower than plain ones and produce a bigger test binary. That is usually irrelevant for a package test and very relevant for a whole-repo CI job. ## A minimal workflow Locally, the loop that actually finds gaps is: run `go test -coverprofile=cover.out ./yourpkg`, open `go tool cover -html=cover.out`, and read the red. Red inside an error branch usually means no test constructs the failure; red inside an exported function usually means nobody calls it from a test at all. In CI, `-func` plus a threshold check is the common shape because it produces a single grep-able number and a per-function breakdown for the log.

  • How do you get the HTML coverage report on a CI machine with no browser?
    Pass an output file: `go tool cover -html=cover.out -o cover.html`. Without `-o`, the tool writes a temporary HTML file and tries to launch a browser, which fails or hangs on a headless runner. With `-o` it just writes the file, which you then upload as a build artifact.
  • Can you merge coverage profiles from two separate go test runs?
    Not with `go tool cover` — the text profile has a single `mode:` header and no merge subcommand, so concatenating two files produces something the tool will not read correctly. Either run one `go test -coverprofile` invocation over every package you want counted, or work with the newer binary counter-data format, which `go tool covdata merge` can combine.
  • What exactly is a 'block' in the coverage profile?
    A basic block: a straight-line run of statements with no branch into or out of it. The toolchain rewrites the source to bump one counter per block, so an `if` body, an `else` body and the code after them are separate blocks. Each profile line records that block's start and end line/column and how many statements it contains.

The percentage is the scoreboard; the profile is the play-by-play. You need the second one to know which part of the field nobody ran onto.

saying these in an interview costs you the question

  • Thinks go test -cover alone writes a report file
  • Calls it line coverage or branch coverage
  • Believes coverage proves the test asserted something
  • Runs go tool cover -html on CI without -o
  • Expects two profiles to concatenate into one
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

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

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