結論
go testの結果キャッシュは、テストバイナリ・cacheable なフラグ・テストが読んだ環境変数・モジュールの中で開いたファイルで照合する。モジュールの外のファイルは、書き換えても(cached)のまま緑になる- 境界はパッケージのディレクトリではなくモジュールルート。外のフィクスチャは、モジュール内に張った symlink 越しに読めば追跡される
-coverprofile/-coverpkg付きの実行もキャッシュされ、カバレッジ率も前回の値が出るGOFLAGS=-count=1は、そのフラグを持たないgo build/vet/list/fmt/runでは無視され、エラーにならない。Makefile で export すれば、全ターゲットのgo testをキャッシュから外せるCGO_ENABLEDを変えると、cgo のファイルを持つnetと、それを import するパッケージがすべて別のキーで作り直される- ビルドキャッシュはユーザ単位で、全リポジトリが共有する。1 つのリポジトリの
go clean -cache/-testcacheが、同じマシンの全プロジェクトに効く
以下は Go 1.26.4 (linux/amd64) で実測した。実験では GOCACHE を一時ディレクトリに向け、手元のキャッシュには触れていない。
前提: テスト結果のキャッシュは何で照合するか

go help test は次のとおり書いている。
The rule for a match in the cache is that the run involves the same test binary and the flags on the command line come entirely from a restricted set of ‘cacheable’ test flags, defined as -benchtime, -coverprofile, -cpu, -failfast, -fullpath, -list, -outputdir, -parallel, -run, -short, -skip, -timeout and -v. If a run of go test has any test or non-test flags outside this set, the result is not cached. (中略) The idiomatic way to disable test caching explicitly is to use -count=1. Tests that open files within the package’s module or that consult environment variables only match future runs in which the files and environment variables are unchanged.
照合に使う入力は 4 つ。
| 入力 | 何が変わるとキャッシュを外れるか |
|---|---|
| テストバイナリ | パッケージと依存のソース、CGO_ENABLED などのビルド設定 |
| コマンドラインのフラグ | cacheable な一覧のフラグの値。一覧に無いフラグ (-count など) が 1 つでもあれば、結果を保存しない |
| テストが読んだ環境変数 | その変数の値。読んでいない変数は関係しない |
| テストが開いたファイル | モジュールの中のファイルの中身。外のファイルは記録しない |
キャッシュが働くのは、./pkg や ./... のようにパッケージを指定した実行 (package list mode) だけ。引数なしの go test は毎回実行する。保存されるのは成功した結果だけ。
テストがどの環境変数とファイルを記録したかは、GODEBUG=gocachehash=1 の testInputs で見られる (ハッシュ値は省略)。
GODEBUG=gocachehash=1 go test ./pkg 2>&1 | grep -E 'testInputs\]: "(open|stat|env) 'HASH[testInputs]: "env APP_MODE <hash>"
HASH[testInputs]: "env GODEBUG <hash>"
HASH[testInputs]: "open /path/to/work/mod/inside.txt <hash>"GODEBUG 自体も読んだ環境変数として記録される。GODEBUG を付けた最初の 1 回は、キャッシュを外れて実行される。
モジュールの外のファイルは書き換えても (cached) のまま

次の構成で試す。テストはモジュールの中のファイルと外のファイルを 1 つずつ読み、環境変数 APP_MODE を参照する。
work/
├── outside.txt # モジュールの外
└── mod/ # モジュールルート (go.mod: module example.com/m / go 1.26)
├── inside.txt # モジュールの中、パッケージのディレクトリの外
└── pkg/
└── a_test.go// mod/pkg/a_test.go
package pkg
import (
"os"
"testing"
)
func TestFiles(t *testing.T) {
for _, p := range []string{"../inside.txt", "../../outside.txt"} {
if _, err := os.ReadFile(p); err != nil {
t.Fatal(err)
}
}
_ = os.Getenv("APP_MODE")
}cd work/mod
go test ./pkg # ok example.com/m/pkg 0.002s
go test ./pkg # ok example.com/m/pkg (cached)
echo v2 > ../outside.txt; touch -d '1 minute ago' ../outside.txt
go test ./pkg # ok example.com/m/pkg (cached) <= 外のファイルは照合しない
echo v2 > inside.txt; touch -d '1 minute ago' inside.txt
go test ./pkg # ok example.com/m/pkg 0.002s <= 中のファイルは照合する
APP_MODE=prod go test ./pkg # ok example.com/m/pkg 0.002s <= 読んだ環境変数
APP_MODE=prod go test ./pkg # ok example.com/m/pkg (cached)
FOO=1 APP_MODE=prod go test ./pkg # ok example.com/m/pkg (cached) <= 読んでいない環境変数touch -d で mtime を過去へずらしているのは、後述の 2 秒の規則を避けるため。
inside.txt はパッケージのディレクトリ (pkg/) の外にあるが、照合される。境界はモジュールルートで、cmd/go/internal/test/test.go は次の条件で記録を飛ばしている。
if a.Package.Root == "" || search.InDir(name, a.Package.Root) == "" {
// Do not recheck files outside the module, GOPATH, or GOROOT root.
break
}契約テストやスナップショット比較で、リポジトリ直下の仕様書やフィクスチャを ../../ で読む構成がこれに当たる。モジュールルートがリポジトリのサブディレクトリにあると、正としているファイルを壊してもテストは緑のまま。
塞ぎ方は 2 つ。
# 1) そのパッケージだけキャッシュを切る
go test -count=1 ./pkg
# 2) モジュールの中に symlink を張り、テストはそこ越しに読む (../testdata_link/data.txt)
ln -s ../outside mod/testdata_link2 は、参照先の outside/data.txt を書き換えると再実行された。gocachehash には open .../mod/testdata_link/data.txt と、モジュールの中のパスで記録される。
go help test に書かれた挙動ではなく、実測の結果なので、頼る前に自分の環境で確かめる。
- PITFALL: モジュールの中で読んだファイルの mtime が 2 秒以内だと、その実行結果はキャッシュに保存されない (
modTimeCutoff = 2 * time.Second。mtime の精度が粗いファイルシステムへの対策)。ファイルを書き換えてすぐ 2 回続けて叩くと 2 回とも実行され、キャッシュが効いていないように見える - PITFALL:
-count=1を./...に付けると、無関係なパッケージのキャッシュまで毎回捨てる。53 パッケージのモジュールで測ると、go test ./...はキャッシュが効いて 0.19s、-count=1付きで 2.29s だった。外のファイルを読むパッケージに絞れるなら絞る
カバレッジ付きの実行もキャッシュされる

パッケージ a (Add とそのテスト) と b (Twice とそのテスト) を持つモジュールで試す。b は a を import しない。
go test -coverprofile=c.out ./...
# ok example.com/c/a 0.002s coverage: 100.0% of statements
# ok example.com/c/b 0.002s coverage: 100.0% of statements
go test -coverprofile=c.out ./...
# ok example.com/c/a (cached) coverage: 100.0% of statements
# ok example.com/c/b (cached) coverage: 100.0% of statements
go test -covermode=atomic -coverpkg=./... -coverprofile=c.out ./... # 2 回目
# ok example.com/c/a (cached) coverage: 50.0% of statements in ./...
# ok example.com/c/b (cached) coverage: 50.0% of statements in ./...-coverprofile は cacheable な一覧に入っている。一覧に無い -covermode と -coverpkg を足しても、キャッシュされた。
(cached) の回も c.out は書き出され、中身は前回の実行の分になる。
前節のモジュールの外のファイルを読むテストも、-coverprofile を付けたまま (cached) で通った。カバレッジのターゲットだけが外のファイルの変更で落ちる、ということは起きない。
-coverpkg=./... では、各テストバイナリが対象の全パッケージを計測する。a/a.go に if 文を 1 つ足すと、a を import していない b のテストも再実行された。
ok example.com/c/a 0.002s coverage: 50.0% of statements in ./...
ok example.com/c/b 0.002s coverage: 25.0% of statements in ./...-coverpkg=./... の実行でキャッシュが当たるのは、モジュールのどのソースも変えていないときだけになる。
- PITFALL: 「カバレッジ付きの実行はキャッシュされない」は Go 1.26.4 では成り立たない。外のファイルを読むテストは、カバレッジ付きでも素のテストと同じく緑のまま通る。カバレッジのターゲットにも
-count=1を付ける
GOFLAGS=-count=1 はテスト以外のサブコマンドで無視される

go help environment は GOFLAGS を次のとおり書いている。
A space-separated list of -flag=value settings to apply to go commands by default, when the given flag is known by the current command. (中略) Flags listed on the command line are applied after this list and therefore override it.
-count を持つのは go test だけだが、他のサブコマンドに渡してもエラーにならない。どのサブコマンドも持たないフラグだけがエラーになる。
GOFLAGS=-count=1 go build ./... # rc=0
GOFLAGS=-count=1 go vet ./... # rc=0
GOFLAGS=-count=1 go list ./... # rc=0
GOFLAGS=-count=1 go fmt ./... # rc=0
GOFLAGS=-count=1 go run . # rc=0
GOFLAGS=-count=1 go test ./pkg # 2 回続けても 2 回とも実行される
GOFLAGS=-bogus=1 go build ./... # go: parsing $GOFLAGS: unknown flag -bogus (rc=1)ターゲットごとに -count=1 を書くと、新しく足したテストのターゲットで付け忘れる。Makefile で export すれば、全ターゲットの go test に効く。
export GOFLAGS := -count=1
test:
go test ./pkg
build:
go build ./...
環境変数なので、gotestsum のように内部で go test を呼ぶツールにも効く。
キャッシュを使いたいターゲットだけは、そのコマンドで GOFLAGS を空にする。
test-cached:
GOFLAGS= go test ./pkg
- PITFALL: コマンドラインで
-countを指定し直しても、キャッシュには戻らない。-count自体が cacheable な一覧に無いので、値が何でも結果を保存しない
CGO_ENABLED を変えると net に依存するパッケージがすべて作り直される

cgo の既定は go doc cmd/cgo が次のとおり書いている。
The cgo tool is enabled by default for native builds on systems where it is expected to work. It is disabled by default when cross-compiling as well as when the CC environment variable is unset and the default C compiler (typically gcc or clang) cannot be found on the system PATH.
gcc の入った linux/amd64 のランナーで GOOS=linux GOARCH=amd64 を付けても、クロスコンパイルではないので cgo は有効のまま。
本体を CGO_ENABLED=0 go build で作るジョブに、変数を付けない go run ./cmd/seed (seed の投入、マイグレーションなど) を足すと、2 つの構成でビルドすることになる。
どこまで作り直すかを、net/http を使う main と、標準ライブラリしか使わない自作パッケージ pure で数える。
// main.go (module example.com/d)
package main
import (
"fmt"
"net/http"
"example.com/d/pure"
)
func main() {
fmt.Println(pure.Hello(), http.StatusOK)
}// pure/pure.go
package pure
func Hello() string { return "hello" }# コンパイラの起動回数を数える
cnt() { go build -x -o /dev/null . 2>&1 | grep -cE '/compile( |$)'; }
CGO_ENABLED=0 cnt # 185 空のキャッシュから
CGO_ENABLED=0 cnt # 0
CGO_ENABLED=1 cnt # 12
CGO_ENABLED=1 cnt # 0
CGO_ENABLED=0 cnt # 0 両方の構成がキャッシュに並ぶ作り直された 12 個は、cgo のファイルを持つ runtime/cgo と net、net を直接か間接に import する crypto/x509 / crypto/tls / net/textproto / mime/multipart / net/http などと main。
pure と fmt は作り直されない。pure のキャッシュのパスは、どちらの構成でも同じになる。
go list -export -f '{{.Export}}' example.com/d/pure # $GOCACHE/ef/ef8e48...-d
CGO_ENABLED=0 go list -export -f '{{.Export}}' example.com/d/pure # 同じパスクラウドの SDK や DB ドライバ、gRPC は net を import するので、実際のアプリでは依存の大半が 2 組になる。
片方の構成だけキャッシュがある状態で測ると、go build ./cmd/seed は 7.59s、CGO_ENABLED=0 を付けると 0.91s だった。
go run 側も CGO_ENABLED=0 に揃える。
CGO_ENABLED=0 go run ./cmd/seedGOOS / GOARCH まで揃えると、別の OS の開発機では go run がその OS で実行できないバイナリを作って失敗する。linux で GOOS=darwin go run . を実行すると次のとおり。
fork/exec /tmp/go-build2411728981/b001/exe/d: exec format error- PITFALL:
CGO_ENABLED=0をジョブ全体に置くと、go test -raceがgo: -race requires cgo; enable cgo by setting CGO_ENABLED=1で失敗する。-raceのターゲットはCGO_ENABLED=1のままにし、その分の 2 組目は受け入れる - PITFALL: 両方の構成がキャッシュにあれば差は出ない。キャッシュの当たり方次第で、遅くなったりならなかったりする
ビルドキャッシュはユーザ単位で、go clean は全プロジェクトに効く

go clean -cache が消すのは go env GOCACHE が指すディレクトリで、既定は $HOME/.cache/go-build。モジュールではなくユーザに 1 つある。
キャッシュのキーはコンパイル単位のハッシュなので、どのモジュールから作った成果物も同じ場所で共有される。
fmt と net/http を import する main を持つ repo1 と repo2 で試す。cnt は前節の関数。
cd repo1 && cnt # 185 空のキャッシュから
cd repo2 && cnt # 1 repo2 の main だけ
cd repo1 && go clean -cache
cd repo2 && cnt # 185 repo1 で消した分を repo2 も作り直すgo clean -testcache も同じ範囲に効く。ビルドキャッシュは残り、テスト結果だけが全リポジトリで無効になる。
cd repo2 && go test . # ok example.com/repo2 (cached)
cd repo1 && go clean -testcache
cd repo2 && go test . # ok example.com/repo2 0.002sall や ci のように毎回叩くターゲットが clean を含み、clean が go clean -cache を呼んでいると、そのたびに同じマシンの全 Go プロジェクトが空のキャッシュからになる。
go env GOCACHE # $HOME 配下のリポジトリ外のパスなら共有資産
make -n all | grep 'go clean' # all から go clean が呼ばれていないか生成物を作り直したいなら、消すのはそのリポジトリの出力先 (bin/、cover.out など) だけにする。テストを実行し直したいなら、-testcache ではなく -count=1 を使う。
CI では、actions/setup-go がキャッシュとして復元するディレクトリにも GOCACHE が入る。ジョブの中で go clean -cache を呼べば、復元した分をそのジョブで捨てる。
- PITFALL: 症状は「手元のビルドが毎回遅い」としか出ない。キャッシュの無い CI では差が出ないので、原因のターゲットと結び付けにくい。3.7GB のキャッシュが消えていた例では、
make allのたびにビルド 15.0s と lint 18.3s を空のキャッシュから払っていた
まとめ
| 場面 | 挙動 | 対策 |
|---|---|---|
| テストがモジュールの外のファイルを読む | 書き換えても (cached) | そのパッケージだけ -count=1。またはモジュール内の symlink 越しに読む |
| テストがモジュールの中のファイルを読む | 中身を照合する。mtime が 2 秒以内なら結果を保存しない | 再現するときは touch -d で mtime を過去へずらす |
| 何が照合されているか知りたい | GODEBUG=gocachehash=1 の testInputs を見る | |
-coverprofile / -coverpkg 付き | キャッシュされ、カバレッジ率も前回の値 | カバレッジのターゲットにも -count=1 |
-coverpkg=./... | 1 パッケージの変更で全パッケージを再実行 | キャッシュが当たるのはソースを変えていないときだけと見込む |
GOFLAGS=-count=1 | テスト以外のサブコマンドは無視し、rc=0 | Makefile で export。キャッシュを使うターゲットは GOFLAGS= で空にする |
CGO_ENABLED が build と run で違う | net を import するパッケージが 2 組できる | go run も CGO_ENABLED=0 に揃える。-race は cgo が要る |
go clean -cache / -testcache | 同じユーザの全リポジトリに効く | 自分の生成物だけを消す。テストの再実行は -count=1 |