skip to content

Build Cache and Reproducibility

The go command keys compiled packages by content hash in GOCACHE, so most rebuilds are lookups, and -trimpath decides whether two machines emit identical bytes.

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

questions

5

Why is a second `go build` of unchanged Go code so much faster than the first, and where does the toolchain keep what it reuses?

level: juniorimportance: must knowfreq 50%

answer

  1. the first build pays, the rest ride free
  2. everything that went in gets hashed
  3. not modification times
  4. one directory under your user cache dir
  5. go env GOCACHE

basics

~20 s

The go command caches compiled package output keyed by a hash of every input. Unchanged code hashes the same, so the second build reuses the stored result instead of recompiling. Running go env GOCACHE prints the directory that holds it.

solid answer

~50 s

Every compile, assemble and link step the `go` command runs is treated as an action, and the action is keyed by a hash of everything that feeds it: the source file contents, the build flags, the target `GOOS`/`GOARCH`, the toolchain version, and the hashes of the results of the packages it depends on. That key is looked up in the build cache — a directory named by `GOCACHE` (`go env GOCACHE` prints it, by default under the user cache directory). On the second build nothing has changed, so every key matches and the stored output is reused rather than recompiled. This is content addressing, not timestamp comparison: touching a file without editing it still hits, and editing one byte misses. `go clean -cache` deletes the whole cache, which is why a CI container that starts empty every run pays the full cold cost each time.

code

text · 7 lines
text
$ go env GOCACHE
/home/alice/.cache/go-build

$ go build ./...     # cold: compiles the packages it needs
$ go build ./...     # warm: every action key matches, nothing recompiles

$ go clean -cache    # empties the build cache; the next build is cold again

go deeper

for a junior

Be ready to say that the go command stores compiled results in a build cache keyed by a hash of the inputs, and to name go env GOCACHE as the way to find it and go clean -cache as the way to empty it.

for a middle

Explain the mechanics: an action key covering source bytes, flags, target platform, toolchain and dependency results, so invalidation happens automatically and timestamps play no part.

for a senior

Show the operational consequence — a CI container starts with an empty cache, so persisting the GOCACHE directory across runs is a large, behaviour-neutral win, and clearing it should be a deliberate act.

for a principal

Frame the cache as an input-keyed guarantee rather than a speed hack: because output is only reused when every input matched, build time can be optimised aggressively without ever putting the correctness of the artefact at risk.

## The problem the cache solves Compiling a Go program means compiling every package it imports, transitively, plus the parts of the standard library it uses. Doing that from scratch on every `go build` would be slow, so the `go` command keeps the results of the work it has already done and reuses them. That store is the **build cache**. ## Actions and action IDs Internally the `go` command turns a build into a graph of *actions*: compile this package, assemble this file, link this binary. Before running an action it computes a hash — commonly called the **action ID** — over everything that could change the action's result: - the contents of the source files (the bytes, not the modification times), - the build flags in effect (`-tags`, `-trimpath`, `-gcflags`, `-race`, and so on), - the target `GOOS`, `GOARCH` and related settings, - the identity and version of the toolchain doing the compiling, - the hashes of the outputs of the packages this one depends on. If that hash is already present in the cache, the recorded output is reused and the compiler is never invoked. If it is not, the action runs and its output is stored under that key. Because dependency output hashes feed into the key, a change deep in the graph correctly invalidates everything above it — you never have to think about invalidation yourself. The stored output itself is also addressed by the hash of its bytes (the *content ID*), so two different actions that happen to produce identical output share one copy on disk. ## Where it lives The directory is named by the `GOCACHE` environment variable. `go env GOCACHE` prints the effective value; the default sits under the operating system's user cache directory (for example `~/.cache/go-build` on Linux and `~/Library/Caches/go-build` on macOS). Inside you will find hash-named subdirectories full of small files — compiled package archives, linker output and metadata. It is an opaque implementation detail: nothing outside the `go` command should read or write it, and its layout is not a stable interface. ## Not timestamps A `make`-style tool compares modification times: if the source is newer than the object file, rebuild. The Go build cache does not work that way, and the difference shows up constantly: - `touch main.go` changes the mtime but not the bytes, so the next build is still a hit. - Checking the repository out fresh gives every file a new mtime, and the build is still mostly a hit as long as the cache directory survived. - Editing a comment *does* change the bytes and *does* cause a rebuild of that package, even though the compiled result may be identical — the cache is keyed on inputs, not on the output you would have got. Because the key covers flags as well as source, building the same package with different flags produces different keys. Two builds that differ only by `-trimpath` do not share entries. ## Emptying and trimming `go clean -cache` removes the entire build cache. That is the deliberate way to force a genuinely cold build — for example when you want to measure how long CI would take on a fresh machine. The `go` command also trims entries that have not been used for a while on its own, so the directory does not grow without bound; you do not need to schedule a cleanup. A cold cache is not a correctness matter, only a speed one: rebuilding from scratch produces the same bytes as a cache hit, because the hit is only reused when every input matched. That is the property that makes the cache safe to trust and makes "clear the cache to be sure" an unnecessary ritual. ## Why this matters in practice On a laptop the cache is warm and invisible. In CI it usually is not: a fresh container has an empty `GOCACHE`, so every run compiles the whole dependency graph plus the standard-library packages it touches. Persisting the `GOCACHE` directory between CI runs — keyed so that a toolchain change starts a new one — is the single largest and safest build-time win available on most Go projects, and it does not change a byte of what the build produces.

  • Does the build cache decide freshness from file modification times?
    No. It hashes the inputs themselves — source bytes, build flags, target platform, toolchain version and dependency results. `touch main.go` changes the timestamp but not the bytes, so the next build still hits. A fresh checkout with all-new timestamps also hits, provided the cache directory survived.
  • Do build flags affect whether you get a cache hit?
    Yes — the flags are part of the key. Building a package once plainly and once with `-trimpath`, `-race` or different `-gcflags` yields different keys and therefore separate entries. Alternating between two flag sets in the same pipeline means each one only ever hits its own half of the cache.
  • Does the build cache grow forever, and how do you empty it?
    It does not: the `go` command trims entries it has not used recently, so no maintenance job is needed. `go clean -cache` deletes everything immediately, which is what you want when deliberately measuring a cold build rather than as routine hygiene.

It is a receipt file rather than a diary: the toolchain does not ask when a file was last touched, it asks whether it has already seen this exact set of inputs before.

saying these in an interview costs you the question

  • Says the cache compares file timestamps the way make does
  • Thinks touching a file forces a recompile
  • Claims stale cache entries are a common cause of wrong builds
  • Believes each project keeps its own cache in the project directory
  • Thinks a cache hit can produce different bytes than a full rebuild
open as a page

What does `go build -trimpath` strip from a Go binary, and why does a reproducible build need it?

level: middleimportance: should knowfreq 40%

basics

~20 s

It removes absolute file system paths from the compiled output. Recorded file names become the module path and version, or the package's plain import path, so builds from different checkout directories produce identical bytes instead of embedding /home/alice or /builds/ci.

open as a page

Two Go builds of the same commit produce binaries with different SHA-256 hashes. How do you make them identical?

level: seniorimportance: should knowfreq 32%

basics

~20 s

Compare the build settings recorded in each binary with go version -m, then pin what differs: the toolchain version, GOOS and GOARCH, CGO_ENABLED, the resolved dependency versions, build tags and GOFLAGS. Add -trimpath so the checkout directory stops leaking in.

open as a page

Your CI runs `go build -a` on every commit for a clean build. What does the flag cost, and is it needed?

level: seniorimportance: should knowfreq 35%

basics

~20 s

The -a flag forces every package to be rebuilt from source, including the standard library, instead of reusing cached results. It buys nothing: the build cache is keyed by a hash of all inputs, so anything that changed already causes a rebuild. Drop it and persist the cache.

open as a page

How does the GOFLAGS environment variable change what `go build` and `go test` do?

level: middleimportance: nice to knowfreq 26%

basics

~20 s

GOFLAGS holds a space-separated list of flags the go command applies by default to any subcommand that knows them, so GOFLAGS=-trimpath makes every build trimmed without editing a single command line. Flags given explicitly on the command line override it.

open as a page