For years I wrote ioutil.TempDir("", "...") and a defer os.RemoveAll at the top of every test that touched the filesystem. Go 1.15 added t.TempDir() and it is strictly better: the directory is scoped to the test, automatically cleaned up at the end of the test (including on failure), and safe to call inside subtests.

Combined with t.Parallel(), the right pattern for table-driven tests that touch the filesystem is:

func TestParseConfig(t *testing.T) {
	t.Parallel()

	cases := []struct {
		name string
		in   string
		want Config
	}{
		{"empty", "", Config{}},
		{"one", "port=8080\n", Config{Port: 8080}},
		{"two", "port=8080\ndebug=true\n", Config{Port: 8080, Debug: true}},
	}

	for _, tc := range cases {
		tc := tc // capture for parallel
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			dir := t.TempDir()
			path := filepath.Join(dir, "config.ini")
			if err := os.WriteFile(path, []byte(tc.in), 0o600); err != nil {
				t.Fatal(err)
			}

			got, err := ParseConfig(path)
			if err != nil {
				t.Fatalf("ParseConfig(%q): %v", tc.in, err)
			}
			if got != tc.want {
				t.Errorf("got %+v, want %+v", got, tc.want)
			}
		})
	}
}

The tc := tc shadow is still needed for Go 1.21 and below. From Go 1.22 on, each loop iteration gets its own variable so you can delete that line. See also /posts/flaky-tests-triage-workflow/.