skip to content

Parallel Cases and Isolation

t.Parallel does not start running side by side at once: it pauses the subtest and resumes it after the parent function returns. That ordering is what makes shared fixtures surprise people.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does calling t.Parallel() in a Go test do, and when does that test's body actually run?

level: juniorimportance: must knowfreq 60%

answer

  1. it is not a go statement
  2. the call returns much later
  3. children wait on the parent's return
  4. post-loop parent code runs first

basics

~20 s

t.Parallel() marks a test as safe to run alongside other tests that also call it. The call pauses the test immediately; it resumes only after its parent test function has returned, and then runs concurrently with its parallel siblings.

solid answer

~40 s

`t.Parallel()` declares that this test may run concurrently with other tests that also call it. Mechanically it is a pause, not a `go` statement: the call blocks, the test is queued as one of its parent's parallel children, and control goes back to the parent. Only when the parent test function returns are the paused children resumed, all at once, capped by `go test -parallel n` (default `GOMAXPROCS`). So in a suite that drives subtests through `t.Run`, the whole loop finishes and the parent returns before any subtest body past `t.Parallel()` executes. Two consequences follow: anything the parent does after `t.Run` happens before the children do real work, and the children then share one process, so a test that needs process-wide state such as `t.Setenv` or `t.Chdir` cannot be parallel at all.

code

go · 12 lines
go
func TestShards(t *testing.T) {
	for _, name := range []string{"a", "b", "c"} {
		t.Run(name, func(t *testing.T) {
			t.Parallel() // pauses here; resumes after TestShards returns
			if got := rowsIn(name); got == 0 {
				t.Errorf("shard %s: no rows", name)
			}
		})
	}
	// This statement runs before any of the three bodies past t.Parallel.
	t.Log("all subtests queued")
}

go deeper

for a junior

Be ready to say plainly that t.Parallel marks a test as concurrent-safe and that the call pauses the test until the enclosing test function returns. Knowing it is opt-in per test is enough at this level.

for a middle

Explain the ordering precisely: the pause, the parent finishing its body, the children resuming together, and the -parallel cap defaulting to GOMAXPROCS. Expect to be asked what a statement after the t.Run loop sees.

for a senior

Show the judgment about what makes a test genuinely parallel-safe: no process-wide state, no shared mutable fixture, no fixed ports or paths. Be able to say what a suite gains in wall-clock time and what it risks in intermittent failures.

for a principal

Frame it as a policy question for a suite: which packages benefit from parallel tests at all, what conventions keep tests independent, and how much latent coupling a team is willing to expose in exchange for a shorter run.

## What the call means `t.Parallel()` on a `*testing.T` is a declaration by the test author: *this test does not depend on being alone in the process, so the `testing` package may run it at the same time as other tests that made the same promise.* It is not a request to run something in the background, and it does not start a goroutine of your own. Only tests that call it participate. A package where three of forty tests call `t.Parallel()` runs thirty-seven tests one after another and those three together. ## The mechanics: it is a pause The surprising part, and the part interviewers actually probe, is that `t.Parallel()` **blocks**. When the call is reached: 1. The test signals its parent that it is a parallel child and gives up the slot it was occupying. 2. The call does not return. Control goes back to the parent test, which continues from just after its `t.Run(...)` call as if the subtest had finished. 3. The parent runs to the end of its own function body. 4. Once the parent's body returns, the `testing` package resumes every paused parallel child, subject to a package-wide limit on how many may run simultaneously. 5. The parent does not *complete* until all of its parallel children have finished; its own teardown runs after them. That is why the code you write after a loop of `t.Run` calls executes **before** the subtests do any of their asserting, and why a variable the parent inspects after the loop still holds its pre-subtest value. The same shape applies to a top-level `TestXxx` that calls `t.Parallel()`: it pauses, the package's sequential tests all run to completion, and then the parallel top-level tests resume together. ## How many run at once The simultaneous count inside one test binary is capped by the `-parallel` flag of `go test`, which defaults to `GOMAXPROCS`. A parent waiting on its parallel children releases its own slot while it waits, so nesting parallel subtests inside a parallel parent does not deadlock even when the limit is 1. ## Why the pause is the right design Because the children only start after the parent's body has returned, the parent gets a clean window to build shared setup, and every child starts from the same well-defined point. It also means the runner knows the full set of parallel siblings before starting any of them, so it can schedule them against the limit instead of discovering them one at a time. ## What it does not give you `t.Parallel()` gives you concurrency, not isolation. All the parallel tests live in the **same process**: the same environment variables, the same working directory, the same package-level variables, the same open files, the same global registries in whatever library you are testing. Anything mutable that two parallel tests can both reach is shared memory, and the ordinary rules about data races apply exactly as they would in production code. That is the reason `t.Setenv` and `t.Chdir` deliberately panic when combined with `t.Parallel()`: they mutate state that belongs to the whole process, and the `testing` package refuses rather than letting one test corrupt another. A fixture built in the parent and referenced from a subtest closure is captured by reference for anything with reference semantics: a map, a slice's backing array, a pointer, an open handle. If two parallel siblings write to it, that is a data race, and for a map it may also abort the whole binary with a `fatal error: concurrent map writes`. ## Practical shape A parallel-safe test typically: builds everything it mutates inside its own closure; treats anything inherited from the parent as read-only; passes configuration as arguments rather than through the environment; and avoids fixed ports, fixed temp paths and shared files. ## Cost and benefit The benefit is wall-clock time on suites dominated by waiting — file reads, network stubs, sleeps. The cost is that any latent sharing between tests, which a sequential run hides, becomes a real race that can fail intermittently. That is a good trade only if the tests are honestly independent; adding `t.Parallel()` to tests that are not is how a fast suite becomes an untrustworthy one.

  • In a test that loops over t.Run with parallel subtests, what runs first: a statement after the loop, or a subtest's assertions?
    The statement after the loop. Each `t.Parallel()` pauses its subtest and returns control to the parent, so the parent's body runs to completion first; the subtests resume only once it returns. Any aggregate the parent tries to read right after the loop is therefore still empty.
  • What happens when a top-level Test function, not a subtest, calls t.Parallel?
    It pauses in the same way. The test binary finishes every non-parallel test in the package first, then resumes all the parallel top-level tests together, up to the `-parallel` limit. Its own parallel subtests, if any, are then run after its body returns.
  • Does calling t.Parallel in a parent test make its subtests run concurrently?
    No. Parallelism is opt-in per test: a subtest runs concurrently with its siblings only if that subtest itself calls `t.Parallel()`. A parallel parent with sequential subtests runs those subtests one at a time, while the parent as a whole runs alongside other parallel tests.

It is like handing your ticket to a queue attendant and stepping aside: you stop where you are, the person behind you carries on, and the whole group you joined is called forward together once the counter is free.

saying these in an interview costs you the question

  • Says t.Parallel starts a goroutine and returns immediately
  • Thinks it makes every test in the package concurrent
  • Expects code after t.Run to run after the subtest asserts
  • Believes each parallel test gets its own process or environment
  • Assumes t.Parallel provides isolation, not just concurrency
open as a page

Why does a Go test that calls t.Setenv panic if it also calls t.Parallel?

level: middleimportance: should knowfreq 42%

basics

~20 s

Environment variables belong to the whole process, so one test's t.Setenv value would leak into every test running at the same time. The testing package therefore panics when a test combines t.Setenv with t.Parallel, in either order.

open as a page

Several Go subtests calling t.Parallel mutate one map the parent test created. How do you diagnose and fix it?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Subtests calling t.Parallel run concurrently after the parent returns, so a map their closures captured is shared, not copied. Confirm with go test -race, then give each subtest its own fixture or guard the shared one with a sync.Mutex.

open as a page

In go test, how many tests run at once when they call t.Parallel, and does that span packages?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

Inside one package's test binary, the number of tests running together is capped by go test's -parallel flag, which defaults to GOMAXPROCS. Separately, go test over several packages runs their binaries at the same time, capped by -p.

open as a page