Cleanup and Temporary Directories in Go Tests Explained
Use t.Cleanup and t.TempDir in Go tests: last-in first-out order, cleanup after a Fatal, helpers that tidy up for callers and directories removed for you.

Tests create things: files, directories, servers, database rows, goroutines. Each needs removing afterwards, and the failures that matter happen when the test fails halfway, which is when manual cleanup is most likely to be skipped. Go's testing package has two small features for this that are worth using everywhere: t.TempDir for files and t.Cleanup for everything else.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
This tip shows both running, in the order they run, including when a test fails. It builds on your first test and t.Helper, and matters most when tests are parallel. Everything ran on Go 1.25.6.
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
var firstDir string
func TestTempDir(t *testing.T) {
dir := t.TempDir() // unique to this test, removed when it ends
firstDir = dir
if err := os.WriteFile(filepath.Join(dir, "data.txt"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
t.Logf("temp dir looks like a unique path under the system temp directory: %v", strings.Contains(dir, "TestTempDir"))
}
func TestTempDirIsGone(t *testing.T) {
_, err := os.Stat(firstDir)
t.Logf("the previous test's directory still exists: %v", err == nil)
}
func TestCleanupOrder(t *testing.T) {
t.Cleanup(func() { t.Log("cleanup 1 (registered first, runs last)") })
t.Cleanup(func() { t.Log("cleanup 2") })
t.Cleanup(func() { t.Log("cleanup 3 (registered last, runs first)") })
t.Log("test body")
}
func TestCleanupRunsAfterFailure(t *testing.T) {
t.Cleanup(func() { t.Log("cleanup still runs after a Fatal") })
t.Fatal("failing on purpose")
}
// A helper registers its own teardown, so the caller needs no matching line.
func newResource(t *testing.T, name string) string {
t.Helper()
t.Logf("open %s", name)
t.Cleanup(func() { t.Logf("close %s", name) })
return name
}
func TestHelperCleanup(t *testing.T) {
newResource(t, "database")
newResource(t, "cache")
t.Run("sub", func(t *testing.T) {
newResource(t, "sub-resource")
t.Log("subtest body")
})
t.Log("parent body ends")
}package main
func main() {}strip() { sed -E 's/ \([0-9.]+s\)//'; }
go test -count=1 -v -run 'TestTempDir|TestCleanupOrder|TestHelperCleanup' . 2>&1 | grep -E '^(=== RUN|--- | clean_test| clean_test)' | grep -v '=== RUN' | strip
echo '--- a failing test'
go test -count=1 -v -run 'TestCleanupRunsAfterFailure' . 2>&1 | grep -E '^(--- | clean_test)' | strip clean_test.go:18: temp dir looks like a unique path under the system temp directory: true
--- PASS: TestTempDir
clean_test.go:23: the previous test's directory still exists: false
--- PASS: TestTempDirIsGone
clean_test.go:30: test body
clean_test.go:29: cleanup 3 (registered last, runs first)
clean_test.go:28: cleanup 2
clean_test.go:27: cleanup 1 (registered first, runs last)
--- PASS: TestCleanupOrder
clean_test.go:47: open database
clean_test.go:48: open cache
clean_test.go:50: open sub-resource
clean_test.go:51: subtest body
clean_test.go:42: close sub-resource
clean_test.go:53: parent body ends
clean_test.go:42: close cache
clean_test.go:42: close database
--- PASS: TestHelperCleanup
--- a failing test
clean_test.go:35: failing on purpose
clean_test.go:34: cleanup still runs after a Fatal
--- FAIL: TestCleanupRunsAfterFailuret.TempDir
t.TempDir() creates a new directory, unique to the test, under the system temporary directory and registers its removal for when the test and its subtests finish. Call it as often as you like: each call returns a different directory. The first test writes a file into its directory and records the path; the second test, which runs afterwards, checks that path and finds nothing: the log says the directory no longer exists. The name includes the test's name (the output confirms the path contains TestTempDir), which helps when you find a leftover directory after a crash.
Use it instead of os.MkdirTemp plus a defer os.RemoveAll: it cannot be forgotten, it works with parallel tests because every test has its own, and it fails the test if creation fails, so there is no error to handle. A related method, t.Chdir(dir) (Go 1.24), changes the working directory for the test and restores it; like t.Setenv, it cannot be used with t.Parallel.
t.Cleanup
t.Cleanup(f) registers f to run when the test completes. It differs from defer in four useful ways.
Order. Cleanups run last-in, first-out, like deferred calls. The TestCleanupOrder output shows the body first, then cleanup 3, 2, 1. This is the right order for resources that depend on each other: close the client before the server it talks to.
After failure. A cleanup runs after t.Fatal, t.FailNow and t.Skip, which stop the test function by calling runtime.Goexit. The failing test shows failing on purpose and then cleanup still runs after a Fatal. (A defer also runs then, but only in the function where it was written.)
Registered from anywhere. A helper function can register a cleanup on the t it is given. The caller does not have to know. newResource logs "open" and registers a "close"; TestHelperCleanup calls it twice and never writes a close.
It waits for subtests. A parent's cleanups run after all of its subtests have finished, including parallel ones, which a defer in the parent does not guarantee. That is why t.Cleanup is the right tool for shared resources in tests with parallel subtests.
Scope: a cleanup belongs to the t it is registered on
The last test shows the scoping. newResource(t, "database") and newResource(t, "cache") register on the parent. Inside the subtest, newResource(t, "sub-resource") registers on the subtest's own t. The log order:
open database, open cache, open sub-resource, subtest body,
close sub-resource,
parent body ends, close cache, close databaseThe subtest's resource closed as soon as the subtest ended, before the parent's body continued, and the parent's resources closed in reverse order at the end. Register cleanups on the narrowest t that matches the resource's lifetime.
Habits
- Put setup and teardown together in a helper that takes
*testing.T, callst.Helper(), creates the thing and registers its cleanup in the same function (fakes and test servers). - Check errors in cleanups with
t.Errorf(notFatal; the test has already ended its body), so a failure to close is reported. - Do not rely on cleanup order across independent resources; if two cleanups depend on order, register them so LIFO gives the right order.
- Keep cleanups fast and idempotent, since they run even when the test failed in a half-built state.
- Use
t.Setenvfor environment variables; it is a cleanup with the original value restored (environment variables in tests). - For setup shared by the whole package, use
TestMain, and remember its cleanup rules differ.


