Go coverprofile

A Go coverprofile is the plain-text file go test -coverprofile writes: a mode: line, then one line per basic block with where the block starts and ends, how many statements it holds, and how many times it ran. It is Go's own coverage format — not LCOV, not Cobertura — and the input to go tool cover.

What is in a file

mode: set
github.com/acme/api/internal/auth/token.go:12.40,14.16 2 1
github.com/acme/api/internal/auth/token.go:14.16,16.3 1 0
github.com/acme/api/internal/auth/token.go:17.2,17.15 1 1
github.com/acme/api/internal/store/db.go:8.30,10.2 2 0
Part Meaning
mode: set How hits were recorded: set, count or atomic
…/auth/token.go The file, as an import path plus file name
12.40,14.16 Block start line.column, end line.column
2 Statements in the block
1 Times it ran (0 or 1 in set mode)

A block is a straight run of code with one entry and one exit — the body of an if, a case, the code after a closing brace. Line 14.16 above ends one block and starts the next one: the if body that never ran.

How the percentage is computed

By statements. Add up the statements in blocks that ran at least once, divide by all statements:

token.go   3 of 4 statements    75.0%
db.go      0 of 2 statements     0.0%
total      3 of 6 statements    50.0%

go test -cover prints this per package (coverage: 50.0% of statements). go tool cover -func=coverage.out breaks it down per function and ends with total: (statements) 50.0%.

The three modes

  • set — did the block run. The default.
  • count — how many times it ran.
  • atomic — counts with atomic increments, correct under parallel goroutines. go test -race switches to it automatically.

The percentage is the same in all three; only the hit counts change. count is what you want for a heat map of hot paths.

The parts that bite

  • Lines are not the unit. One line can belong to two blocks — the if err != nil { line is in the parent block, while the body that starts on the same line may never have run. Tools that convert to line coverage (gcov2lcov, Codecov) have to decide what that line is, which is why their numbers drift from go test -cover.
  • Only tested packages appear. By default each package is measured by its own tests. Code exercised from another package's tests counts only with -coverpkg=./..., and packages with no test files can be missing entirely, which inflates the total.
  • Merged profiles repeat blocks. -coverpkg and concatenated runs list the same block more than once. go tool cover merges them; hand-rolled scripts that just sum statements double-count.
  • Import paths, not repo paths. Lines start with the module path (github.com/acme/api/…), so tools that map files to the repository need the module prefix stripped.
  • Binary coverage since Go 1.20. go build -cover plus GOCOVERDIR writes binary covmeta/covcounters files for integration tests. go tool covdata textfmt -i=<dir> -o=coverage.out turns them back into a coverprofile.

Reading one without go tool cover

go tool cover -html=coverage.out -o coverage.html writes annotated source — the best view of which branch is untested. For the summary a pull request needs, the free coverage viewer reads coverage.out in the browser: the same statement total as go tool cover -func, a roll-up per package directory, files sorted least-covered first with their uncovered line ranges, and a Markdown table to paste.

Try it

Comma is free — unlimited reports, unlimited commenters, unlimited revision history.

Open the coverage viewer →

Related