[!IMPORTANT] To clear any misunderstanding: this proposal does not change the semantics of any existing test.

Proposal Details

https://github.com/golang/go/issues/21214 proposed the same but was declined -- mostly because at the time we had no good way of rolling out such a change safely. As this is not the case anymore, and the benefits keep increasing with time (core counts are increasing, codebases and test suites are growing, etc.), I think it would be worth reconsidering it.

The idea would be to make parallel execution the default for modules that specify a go version (not toolchain version) higher than v1.X. Modules with go version below v1.X would still default to serial execution, as today. This would mean that behaviour would not change until a module go version (not toolchain version) is manually updated to v1.X or later.

Individual tests would be given a way to opt-out of parallel execution, similar to how today we allow individual tests to opt-in to parallel execution.

A one-off migration executed automatically when the module is upgraded to a go version after v1.X would ensure that existing semantics (serial execution) are maintained.

I encourage reviewers to focus on the goal when discussing this proposal, i.e. I welcome alternative ways/solutions to achieve the same goal, and will gladly update the proposal if a better solution is suggested. Some discussion is happening in https://gophers.slack.com/archives/C0VP8EF3R/p1747791714490489.

Proposal

  • Add testing.T.Serial() to allow individual tests to opt out of the new default. Mention that Serial was the default before go v1.X. No-op if the default is serial execution. Panics or fails the test if called after Parallel(). If present, it must be the first statement in the test function.
  • Update the doc for testing.T.Parallel() to mention that this is the default starting with go v1.X. No-op if the default is parallel execution. Panics or fails the test if called after Serial().
  • For modules that specify in go.mod a go version equal or greater than v1.X, run the tests in parallel by default; modules that specify an older version would still run tests serially be default.
  • When a module's go version is updated with go mod edit -go=1.Z5 from a v1.Y < v1.X to a version v1.Z >= v1.X, a one-off migration is performed4 on the module, adding Serial() calls to any test that does not have a Parallel() call, and removing Parallel() calls from any test that has it3.
  • Linters and other tools could start flagging t.Parallel() calls as unneeded for modules with a go.mod go version equal or greater than v1.X.

To guarantee the ordering of serial tests, the compiler should recognize test functions that call t.Serial() and arrange for them to be executed in the order in which they appear in the file. (This is currently the ugliest implementation detail of this proposal; alternatives are most welcome).

FAQ

Will I be forced to rewrite my tests to be runnable in parallel?

No.

A migration tool would automatically turn existing tests like:

func TestA(t *testing.T) {
  // ...
}

func TestB(t *testing.T) {
  t.Parallel()
  // ...
}

into:

func TestA(t *testing.T) {
  t.Serial()
  // ...
}

func TestB(t *testing.T) {
  // ...
}

thus maintaining the current semantics, even after the migration to the new go version.

For cases in which t.Parallel() is not the first statement with side effects in the function there are two options: either bail out of the migration and warn the user that manual migration is required (but this is pretty disruptive), or drop the t.Parallel() and as a fallback add a t.Serial(), printing a warning about this (as it may slow the test suite).

Users could then, over time, drop the t.Serial from tests that do not really need them.

Worth pointing out that, even without version control, this change is trivially reversible3.

Will this break my tests?

No. (see previous answer for details)

Why should tests be executed in parallel by default?

Because serial tests:

  • can accidentally hide hidden dependencies between tests
  • can accidentally hide data races
  • implicitly endorse the use of global state
  • are slower to run when part of a large test suite2

Parallel tests being the default nudge the whole ecosystem in a better direction, without forcing anyone to have to write parallel tests.

Isn't this going to be disruptive?

Statistics indicate that the vast majority of modules do not specify the most recent go version (1.24.*), or even just a go module version that was released in the last year.

go.mod go version module count1
1.24 58.1k
1.23 179k
1.22 177k
1.21 150k
1.20 101k
1.19 88.1k
1.18 90.6k
1.17 82.9k
1.16 92.2k
1.15 90.1k
1.14 68.9k
1.13 77.3k
1.12 42.0k
1.11 4.4k
1.10 0.2k
1.9 0.2k
1.8 <0.1k

This would seem to suggest that most existing modules will not be migrated to a go module version where the default is parallel execution. For modules that are migrated, the automated migration would make sure that existing semantics are maintained, therefore not requiring maintainers to spend any extra effort during the migration or later - unless they want to make use of parallel test execution (but this is orthogonal to the change, as it would be the case even if this proposal is not enacted).

I can acknowledge that some users that do not follow go development may be surprised by the new t.Serial() calls added by the migration tool, but I would suggest that a quick online search would almost certainly lead them to an article that explains the rationale.

All the above considered, I would argue that the change may be at most characterized as "surprising", but not really "disruptive".

Comment From: gabyhelp

Related Issues

Related Code Changes

(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)

Comment From: dolmen

Compatible test suites can already opt-in by injecting t.Parallel() progressively in all tests. But I guess that many test suites are not prepared for that to be applied all at once for all tests.

When a module upgrades the Go version, that's usually to use new features of a newer Go, such as new features of the stdlib. Nobody wants to be forced to rewrite his testsuite for parallel testing compliance as well just because of go mod edit -go=....

So I'm against this proposal.

However, as someone who also has already attempted to upgrade a testsuite to massively use parallel testing in top level tests (running with go test -parallel=50 -race), I think that a go test flag that would change the default would be helpful to quickly evaluate the effort needed without having to edit all tests to add t.Parallel() everywhere: -parallelforce?

Comment From: CAFxX

Nobody wants to be forced to rewrite his testsuite for parallel testing compliance

@dolmen the proposal already hints at how to avoid this problem, though. Let me try to make it clearer.


Update: tweaked the proposal.

Comment From: dharmab

As I understand the tweaked proposal, maintainers of source code would need to modify sources to migrate. I think this is at odds with https://go.dev/doc/go1compat. Would this would to be a Go 2 proposal, rather than a Go 1.XX proposal?

EDIT: Had to read it again with more tea in me - yes that is covered in the proposal.

Comment From: CAFxX

@dharmab yes, an automated tool would do the migration in order to maintain the current semantics for legacy code. This tool runs only once per module, when the go module version is increased to a version equal or greater to the one where the new default applies. For this proposal users do not need to do anything beyond whatever they would need to do anyway to bump the go module version (whenever they decide to do it for their own reasons).

Comment From: peczenyj

tests that set env vars should not be executed in parallel. this feature will introduce many false negatives until someone realizes that they need to optout parallel tests

BTW there are some linters that can check and warn about missing the t.Parallel()

Comment From: CAFxX

@peczenyj this proposal will not cause the problem that you describe because any existing test will continue to be run serially - even after 1.X. Only new tests written in modules that already declare a version of go 1.X or higher will need to be written knowing that they will run in parallel unless t.Serial() is called.

FWIW, as the proposal mentions, discouraging (moving forward) the reliance by default on specific ordering of tests and on global state is an intended consequence.


Update: I added a large warning at the top of the proposal, and reworded things for clarity, to hopefully avoiding further misunderstandings.

Comment From: peczenyj

It means if I update my go.mod to go 1.XX I need to opt out this new feature OR my existing tests will starts to fail (because the go compiler does not know of they are new or not). Perhaps “go fix” may force an opt out in case of missing explicit Paralle or Serial

Perhaps a better approach is:

Make set environment trigger serial tests (but this may happens deeper in the test), or

By calling Parallel or Serial it affects all subtests (this helps avoid copy paste on nested tests), or

Add go test argument —-force-parallel that can be triggered by an env var

Anyway this feature should be EXPERIMENTAL imho

Comment From: CAFxX

The proposal describes how this will work:

When a module's go version is updated from a v1.Y < v1.X to a version v1.Z >= v1.X, a one-off migration is performed1 on the module, adding Serial() calls to any test that does not have a Parallel() call, and removing Parallel() calls from any test that has it2.

So there's nothing to opt-in or opt-out of. When the version is bumped the migration automatically runs and all tests that exist at the time of the migration are migrated, thus ensuring that semantics of the existing tests are preserved.

To reiterate, this proposal will NOT change the behavior of any existing test -- contrary to (past) proposals that suggested adding flags or environment variables to control this behavior, and that were rejected for reasons that are still valid today.

Comment From: Merovius

@CAFxX

When a module's go version is updated from a v1.Y < v1.X to a version v1.Z >= v1.X, a one-off migration is performed

One note is, that there really is no way to do "a one-off migration when a module's go version is updated". The go tool only knows what the state of the module is when it runs. It only sees a tree, not its history. You can use the go tool to update the Go version in go.mod, but there is no guarantee that it is used.

So I assume that the semantics you are imagining are "when go test is run and the Go version in go.mod is ≥v1.X, it adds a t.Serial() line to every test which has neither a t.Serial() nor a t.Parallel() line". If you imagine something else, feel free to correct me.

As far as I know, there is no precedent for go test (or go build or go install) to modify Go source code. There is precedent for it editing go.sum (and/or go.mod?), but not Go code itself. There also is precedent for go fix to modify Go code.

So I think it needs to be explicit that either

  1. this proposal assumes that people must run go fix at some point (possibly between upgrading from Go 1.X to Go 1.(X+N)). Or
  2. this proposal creates precedent of go test automatically editing Go source code.

I think that not being explicit is why this is being repeatedly brought up.

Furthermore, t.Parallel can theoretically appear at arbitrary points in a function, as well as in functions indirectly and/or dynamically called from test functions (playground). Conventionally, it appears at the top of the test function, but there is no guarantee. This is a complication that you should at least talk about in more depth, in my opinion. It's notably not a problem with editing go.mod or go.sum themselves. Those are fully declarative and easier to automatically edit.

To me, this complication makes Option 2 seem a bad idea. At the very least, it doesn't seem obvious that Option 2 is even robustly workable. And I also don't think Option 1 is something that we want to rely on, as go fix isn't very commonly run these days. If we where willing to rely on go fix in this way, we certainly could have done the loop variable change far more robustly.

So, as others, I do think there needs to be at least a bit more explanation of how this proposal is supposed to be workable. I don't think the description that you gave so far is enough to be confident that there is no breakage.

Comment From: CAFxX

The go tool only knows what the state of the module is when it runs. It only sees a tree, not its history.

@Merovius I am aware of this. I was thinking the go mod edit -go=1.Z would be the one triggering the migration. It was mentioned, but got lost in one of the edits. Will clarify.

I was also thinking, but did not write it out to avoid increasing the scope too much, that this could be relaxed if go.sum (or possibly a different file, because I suppose go.sum's purpose is somewhat different) stored the go version for the module as well: in this way we could detect when a user has manually modified the go version in go.mod. This would definitely help whenever such a migration is needed.


I updated the proposal also for the point about how to handle functions that call Parallel() not at the beginning; thanks for pointing it out! This actually raised a bit of a tricky point for which I only have a pretty ugly answer: how to maintain the order of non-parallel tests. I added a blurb about how this could be done (the compiler would need to handle functions that call t.Serial differently) but it's a quite inelegant so suggestions are welcome.


  1. Module count obtained on 2025-05-29 by searching on public Github repos using the search query language:"Go Module" /^go 1\.x/ where x is replaced by the go minor version (e.g. 24 for 1.24) 

  2. This is further made relevant by the increasing core counts of modern infrastructure. Making use of the available cores to reduce test duration is much easier if most/all tests are executed in parallel. While it is true that go test is normally already able to parallelize test execution across packages, this does not really help much in a number of scenarios, e.g. when there are few packages to test (common in edit/test cycles), or when a package takes significantly longer to be tested than all others. 

  3. This migration is trivially reversible even without version control by adding a Parallel() call to any function without Serial() call, and removing all Serial() calls. 

  4. Whether the migration is performed silently, or whether the user is prompted to confirm they want to run the migration, is a UX implementation detail. 

  5. We could detect also manual edits of go.mod if the module's go version was added to go.sum (or a different "lock" file). In this way the go tool could notice manual modifications of the go version, and either direct the user to run the migration using a dedicated command, or prompt the user that to continue the action a migration is required that will be executed if the user accepts.