The CI job is red. The log says report differs from golden. The test wrote the rendered report and a diff to disk before it failed, and you would give a lot to open those two files. You can't. They lived in t.TempDir(), and the testing docs are clear about what happens next: "The directory is automatically removed when the test and all its subtests complete." The runner is gone too.

So you rerun locally, fail to reproduce it, add a t.Log that dumps 200 lines of report into the job output, and push again.

Go 1.26, released in February 2026, ships a standard answer. T.ArtifactDir, B.ArtifactDir and F.ArtifactDir "return a directory in which to write test output files (artifacts)." Pass -artifacts to go test and that directory survives under -outputdir. Leave the flag off and it behaves like a temp dir that gets cleaned up. Same test code either way.

What ArtifactDir promises

The method docs are short:

  • With -artifacts, the directory "is located under the output directory." Without it, ArtifactDir "returns a temporary directory that is removed after the test completes."
  • "Each test or subtest within each test package has a unique artifact directory."
  • "Repeated calls to ArtifactDir in the same test or subtest return the same directory." That is the opposite of TempDir, which hands you a fresh directory on every call.
  • "Subtest outputs are not located under the parent test's output directory."

The design came from proposal #71287, accepted in April 2025. Its motivation is the CI case: "Some tests produce output files which the user may want to examine." Examples raised in the proposal discussion were broken execution traces, pprof output and core dumps that "can be easily downloaded and dissected."

With -artifacts set, the directories are kept for every test that called ArtifactDir, pass or fail. The flag means "keep," not "keep on failure." If you only want evidence from failures, only call ArtifactDir on the failure path. The test below does exactly that.

A test file sliding toward a shredder while a burnt-orange crate catches its output files
TempDir sends a failing test's files to the shredder. ArtifactDir with -artifacts keeps them.

A golden-file test that saves its own evidence

Here is a small package that renders an order report. Module path example.com/shop, package in report/.

// report/report.go package report import ( "fmt" "io" "sort" ) // Line is one row of an order report. type Line struct { SKU string Qty int Cents int } // Render writes a fixed-width report sorted by SKU. func Render(w io.Writer, lines []Line) error { sort.Slice(lines, func(i, j int) bool { return lines[i].SKU < lines[j].SKU }) total := 0 for _, l := range lines { sub := l.Qty * l.Cents total += sub _, err := fmt.Fprintf(w, "%-8s %3d %8.2f\n", l.SKU, l.Qty, float64(sub)/100) if err != nil { return err } } _, err := fmt.Fprintf(w, "%-8s %3s %8.2f\n", "TOTAL", "", float64(total)/100) return err } 

The test compares the output to a golden file. On a match it returns and never touches ArtifactDir. On a mismatch it writes both versions into the artifact directory and names the path in the failure message.

// report/report_test.go package report import ( "bytes" "flag" "os" "path/filepath" "testing" ) var update = flag.Bool("update", false, "rewrite golden files") func TestRender(t *testing.T) { lines := []Line{ {SKU: "B-200", Qty: 2, Cents: 1250}, {SKU: "A-100", Qty: 1, Cents: 999}, } var got bytes.Buffer if err := Render(&got, lines); err != nil { t.Fatal(err) } golden := filepath.Join("testdata", "report.golden") if *update { writeFile(t, golden, got.Bytes()) } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if bytes.Equal(got.Bytes(), want) { return } dir := t.ArtifactDir() writeFile(t, filepath.Join(dir, "got.txt"), got.Bytes()) writeFile(t, filepath.Join(dir, "want.txt"), want) t.Errorf("report differs from %s; see %s", golden, dir) } func writeFile(t *testing.T, path string, b []byte) { t.Helper() if err := os.WriteFile(path, b, 0o644); err != nil { t.Fatal(err) } } 

Create the golden file once, then run the test normally:

mkdir -p report/testdata go test ./report -run TestRender -update go test ./report 

Now break it on purpose. Change %8.2f to %9.2f in both Fprintf calls and run the test again. It fails, and without -artifacts the got.txt and want.txt it wrote are deleted with the temp directory, exactly like t.TempDir(). Your test code didn't have to care.

Running with -artifacts and reading the ARTIFACTS line

Add the flag:

go test ./report -v -artifacts 

The go help testflag text says -artifacts will "Save test artifacts in the directory specified by -outputdir," and -outputdir defaults to "the directory in which "go test" is running." Inside it, the test binary joins _artifacts, per the testing source: toOutputDir("_artifacts"). So a run from the module root leaves an _artifacts/ folder next to your go.mod.

The first ArtifactDir call in a test also writes the location to the test log. The release notes give the shape for a test named TestArtifacts:

=== ARTIFACTS TestArtifacts /path/to/artifact/dir 

That is why the command above has -v. In the source, the line goes through the verbose ("chatty") printer, guarded by if c.chatty != nil, so a non-verbose run won't show it. Your own t.Errorf message still carries the path, which is one reason to put dir in it.

Two details from the docs that change how you use this. First, -artifacts is not on the list of cacheable test flags (-benchtime, -coverprofile, -cpu, -failfast, -fullpath, -list, -outputdir, -parallel, -run, -short, -skip, -timeout and -v), and "any test flag or argument other than the cacheable flags" disables test caching, so runs with it execute every time. Second, the folder name starts with _, and ./... skips "Files and directories with names beginning with "_" or "."", so go test ./... won't wander into old artifacts. Add _artifacts/ to .gitignore anyway; the proposal says "Existing artifact directories would never be overwritten or deleted," so they pile up locally.

Uploading the directory from GitHub Actions

In CI the runner is thrown away, so the files have to leave the machine. actions/upload-artifact does that.

name: test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v7 with: go-version-file: go.mod - name: go test shell: bash run: go test -v -artifacts ./... 2>&1 | tee test.log - uses: actions/upload-artifact@v7 if: failure() with: name: go-test-artifacts path: | _artifacts/ test.log if-no-files-found: ignore retention-days: 7 
  • go-version-file: go.mod makes actions/setup-go install the version your module declares, which needs to be 1.26 or later for ArtifactDir to exist.
  • shell: bash changes behaviour here. GitHub's workflow syntax docs show that an unspecified shell on Linux runs bash -e {0}, while an explicit shell: bash runs bash --noprofile --norc -eo pipefail {0}. Without pipefail, tee succeeds and swallows the failing exit code of go test.
  • if: failure() returns true "when any previous step of a job fails," so green runs upload nothing.
  • if-no-files-found: ignore keeps the upload quiet when an earlier step, like setup-go, failed before test.log existed. The upload-artifact default is warn.
  • test.log goes along because on Go 1.26 it is the simplest reliable map from directory to test. Next section.

A conveyor carrying a crate out of a factory building to a shelf of stored crates
The runner is disposable. The uploaded artifact is what survives the job.

The path names: random on 1.26, readable on 1.27

The proposal specified a layout of _artifacts/<test package>/<test name>/<random>. Go 1.26.0 didn't produce it. Issue #77763, filed in February 2026 against go1.26.0, reports an === ARTIFACTS TestArtifacts line whose path ended in tmp/_artifacts/2595644996, where the reporter expected a directory derived from the test name.

The reporter traced it to makeArtifactDir: the code built a base path that started with /, and filepath.Localize() "explicitly rejects any path that starts with a forward slash," so the package and test parts were dropped. Every test's directory landed flat under _artifacts/ with a random number for a name.

The fix removed the leading slash and also handled tests in a module's root package, which the old code got wrong. The issue was closed on March 6, 2026, with the milestone set to Go1.27. None of the go1.26.x point release notes, through go1.26.10, mention the testing package, so assume 1.26 still has the flat layout.

On Go 1.27 (August 2026), the source builds the path like this:

  • The package part is the import path with the module path trimmed off, so example.com/shop/report becomes report. A test in the module root gets no package part.
  • Subtest slashes become __, so TestRender/empty becomes TestRender__empty.
  • Names longer than 64 characters are cut and finished with a hash.
  • A random directory is still created at the end with os.MkdirTemp, so repeated runs don't collide.

For the test above that means _artifacts/report/TestRender/<random>/got.txt. On 1.26 you get _artifacts/<random>/got.txt and need the === ARTIFACTS line in test.log to know which test wrote it.

One more 1.27 change matters here. go test now runs the stdversion vet check by default, which "reports the use of standard library symbols that are too new for the Go version in force." The analyzer's doc only carves out fields and methods of types that are themselves too new, and testing.T is not new, so a module whose go.mod says go 1.25 and calls t.ArtifactDir() is squarely in scope. Bump the go directive first.

A few habits that keep it tidy

Call ArtifactDir only on the path that produces evidence. A passing test that never calls it creates no per-test directory and prints no log line, even with -artifacts. You may still see an empty _artifacts/ folder.

Call it once per test and keep the result in a variable. Repeated calls return the same directory, so nothing breaks if you call it twice, but one variable makes the failure message easy to write.

Put the path in your failure message. It survives a non-verbose run and shows up in the Actions log.

Keep using t.TempDir() for scratch files a test needs while running, like a SQLite file or a socket. ArtifactDir is for files a human might want to read after the test is over.

If this was useful

ArtifactDir is a small API that sits on how go test builds a test binary and hands it flags. If you want that layer of Go to feel familiar, The Complete Guide to Go Programming covers the language, the runtime and the standard library at that depth. It is book one of the Thinking in Go series.

Thinking in Go — the 2-book series on Go programming and hexagonal architecture