Go Cheatsheet
Testing
Use this Go reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Running Tests
go test is the whole test runner. No config file, no external framework.
go test ./... # every package, recursively go test -v ./pkg/store # verbose, print every t.Log go test -run 'TestParse' # regexp on the test name go test -run 'TestParse/empty_input' # one subtest (spaces became underscores) go test -skip 'TestSlow' ./... # inverse of -run go test -count=1 ./... # the only way to bypass the test cache go test -race ./... # data race detector, use it in CI go test -timeout 30s ./... # panic-dump the whole binary after 30s go test -short ./... # sets testing.Short() go test -failfast ./... # stop after the first failing test go test -shuffle=on ./... # randomize order, catches shared state go test -json ./... > out.json # machine-readable events go test -list '.*' ./pkg/store # list test names, run nothing
Coverage and benchmarks:
go test -cover ./... go test -coverprofile=cover.out -covermode=atomic ./... go tool cover -html=cover.out # annotated source in a browser go tool cover -func=cover.out | tail -1 # total percentage go test -bench=. -benchmem ./... # all benchmarks, alloc stats go test -bench=BenchmarkParse -benchtime=5s go test -bench=BenchmarkParse -benchtime=1000x # exactly 1000 iterations go test -fuzz=FuzzParse -fuzztime=30s ./pkg/parse
-covermode=atomic is required whenever you combine -race with coverage, since
the default set mode is not race-safe. Test results are cached, and -count=1
is the documented way to force a rerun.
File and Package Layout
store/ store.go store_test.go // package store — internal, sees unexported ids export_test.go // package store — expose internals to the ext test api_test.go // package store_test — external, only the public API testdata/ // ignored by the go tool, put fixtures here golden/parse.json
- A test file must end in _test.go and is excluded from the normal build.
- package foo tests reach unexported identifiers. package foo_test in the same
directory can only use the exported API, which is the honest way to test that
your package is usable, and it breaks import cycles.
- testdata is skipped by every go command, so fixtures live there.
- Test function signature: func TestXxx(t *testing.T), and the name after Test
must not start with a lowercase letter.
The testing.T API
| Method | Effect |
|---|---|
t.Error / Errorf | mark failed, keep running |
t.Fatal / Fatalf | mark failed, stop this test now |
t.Log / Logf | output, shown only with -v or on failure |
t.Skip / Skipf / SkipNow | skip the rest of this test |
t.Helper() | report the caller's line, not this function's |
t.Cleanup(fn) | run fn when the test ends, LIFO, even on failure |
t.TempDir() | a fresh directory, removed automatically |
t.Setenv(k, v) | set an env var, restored after (forbids t.Parallel) |
t.Chdir(dir) | change the working dir, restored after (1.24+) |
t.Context() | a context cancelled just before Cleanup runs (1.24+) |
t.Parallel() | run this test alongside other parallel ones |
t.Run(name, fn) | a subtest |
t.Name() | the full test name, e.g. TestParse/empty |
t.Deadline() | the -timeout instant, if set |
func TestWriteFile(t *testing.T) { dir := t.TempDir() // cleaned up for you t.Setenv("APP_ENV", "test") // restored for you path := filepath.Join(dir, "out.txt") if err := WriteFile(path, "hi"); err != nil { t.Fatalf("WriteFile(%q) = %v, want nil", path, err) } } // Helpers must call t.Helper(), or every failure points at the helper func mustDecode(t *testing.T, b []byte) Config { t.Helper() var c Config if err := json.Unmarshal(b, &c); err != nil { t.Fatalf("decode config: %v", err) } return c }
t.Fatal calls runtime.Goexit, so it only works on the test's own goroutine.
From a spawned goroutine use t.Error and return.
Table-Driven Tests
This is the canonical Go test. One slice of cases, one loop, one subtest each.
func TestParseDuration(t *testing.T) { tests := []struct { name string in string want time.Duration wantErr bool }{ {name: "seconds", in: "30s", want: 30 * time.Second}, {name: "minutes", in: "5m", want: 5 * time.Minute}, {name: "compound", in: "1h30m", want: 90 * time.Minute}, {name: "empty", in: "", wantErr: true}, {name: "garbage", in: "abc", wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := ParseDuration(tt.in) if tt.wantErr { if err == nil { t.Fatalf("ParseDuration(%q) = %v, want error", tt.in, got) } return } if err != nil { t.Fatalf("ParseDuration(%q) unexpected error: %v", tt.in, err) } if got != tt.want { t.Errorf("ParseDuration(%q) = %v, want %v", tt.in, got, tt.want) } }) } }
Message convention: got = X, want Y, with the input echoed. A failure should be
readable without opening the test.
For structs and slices, compare with reflect.DeepEqual or, better,
github.com/google/go-cmp/cmp:
if diff := cmp.Diff(tt.want, got); diff != "" { t.Errorf("Parse(%q) mismatch (-want +got):\n%s", tt.in, diff) }
Subtests and Parallel
t.Run("group", func(t *testing.T) { t.Run("case a", func(t *testing.T) { /* ... */ }) t.Run("case b", func(t *testing.T) { /* ... */ }) })
Subtest names get spaces replaced by underscores, so target them as
-run 'TestParse/case_a'.
t.Parallel() pauses the subtest until its parent returns, then runs all paused
siblings together, up to -parallel (default GOMAXPROCS).
- Pre Go 1.22, for _, tt := range tests shared one loop variable, so every
parallel subtest saw the last case. The fix was tt := tt inside the loop. Go
1.22 made each iteration its own variable and the copy is now unnecessary, but
only for modules declaring go 1.22 or later in go.mod.
- Parent code after the t.Run loop runs before any parallel subtest
finishes. Put shared teardown in t.Cleanup, never after the loop.
- t.Setenv and t.Chdir panic in a parallel test, since they mutate process
state.
TestMain, Fakes, and Golden Files
func TestMain(m *testing.M) { flag.Parse() // needed if you define custom flags if err := startDB(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } code := m.Run() stopDB() // deferred functions do NOT run before Exit os.Exit(code) }
Fakes come from interfaces, no mocking library required:
type Store interface { Get(ctx context.Context, id string) (Item, error) } type fakeStore struct { items map[string]Item err error } func (f fakeStore) Get(_ context.Context, id string) (Item, error) { if f.err != nil { return Item{}, f.err } it, ok := f.items[id] if !ok { return Item{}, ErrNotFound } return it, nil }
Golden files keep large expected output out of the source:
var update = flag.Bool("update", false, "rewrite golden files") func TestRender(t *testing.T) { got := Render(input) golden := filepath.Join("testdata", "render.golden") if *update { if err := os.WriteFile(golden, got, 0o644); err != nil { t.Fatal(err) } } want, err := os.ReadFile(golden) if err != nil { t.Fatal(err) } if !bytes.Equal(got, want) { t.Errorf("Render mismatch, rerun with -update to accept:\n%s", got) } }
Refresh them with go test ./... -update, then read the diff before committing.
Benchmarks
// Go 1.24+ : b.Loop handles timing and keeps the compiler honest func BenchmarkParse(b *testing.B) { data := loadFixture(b) for b.Loop() { _, _ = Parse(data) } } // The classic form, still everywhere func BenchmarkParseOld(b *testing.B) { data := loadFixture(b) b.ReportAllocs() b.ResetTimer() // drop setup cost from the measurement for i := 0; i < b.N; i++ { _, _ = Parse(data) } } func BenchmarkParallel(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { _, _ = Parse(data) } }) }
b.Loop is the 1.24 replacement for the b.N loop. Setup before it is excluded
from the timing automatically, so ResetTimer is unnecessary, and it prevents the
compiler from optimizing away a call whose result you discard. Other tools:
b.StopTimer/StartTimer around per-iteration setup, b.ReportMetric(v, "ops")
for a custom unit, and benchstat on two -count=10 runs to know whether a delta
is real.
Fuzzing
func FuzzParse(f *testing.F) { f.Add("30s") // seed corpus, types fix the fuzz signature f.Add("1h30m") f.Add("") f.Fuzz(func(t *testing.T, in string) { d, err := ParseDuration(in) if err != nil { return // rejecting bad input is correct } // property: round-tripping must be stable got, err := ParseDuration(d.String()) if err != nil || got != d { t.Errorf("round trip %q -> %v -> %v, %v", in, d, got, err) } }) }
Run with go test -fuzz=FuzzParse -fuzztime=30s. Without -fuzz it only runs the
seed corpus, so it behaves like a normal test in CI. A crash is written to
testdata/fuzz/FuzzParse/, commit that file and it becomes a permanent
regression case. Fuzz targets accept []byte, string, the numeric types, bool
and rune only.
Examples
An Example function is documentation that the test runner verifies.
func ExampleParseDuration() { d, _ := ParseDuration("1h30m") fmt.Println(d) // Output: 1h30m0s } func ExampleClient_Get() { // attaches to Client.Get in the docs // ... // Unordered output: // b // a }
The // Output: comment is the assertion, compared after trimming leading and
trailing space on each line. Drop the comment and the example compiles but is
never run. Naming is Example, ExampleF, ExampleT, ExampleT_M, with an
optional lowercase suffix like ExampleParseDuration_hours.
Assertions and Vet
The standard library gives you if and t.Errorf. That is the idiom.
// Errors: match by identity or type, never by string if !errors.Is(err, ErrNotFound) { t.Fatalf("Get() error = %v, want ErrNotFound", err) } var perr *ParseError if !errors.As(err, &perr) { t.Fatalf("Get() error = %T, want *ParseError", err) } if perr.Offset != 4 { t.Errorf("Offset = %d, want 4", perr.Offset) }
go vet runs automatically as part of go test with a subset of its checks
(printf, tests, atomic and a few more). Run the full set with go vet ./..., and
add staticcheck for anything deeper.
github.com/stretchr/testifyis very common and itsrequire.NoErrorreads nicely. The standard library is genuinely enough, and a table test with plainifblocks has no dependency and no magic. Pick one per repo and stay consistent.