Description
Important
To clear any misunderstanding: this proposal aims at not changing the semantics of existing tests.
Proposal Details
#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 afterParallel()
. 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 afterSerial()
. - 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 tidy
(when it triggers the bump) orgo mod edit -go=1.Z
1 from a v1.Y < v1.X to a version v1.Z >= v1.X, a one-off migration is performed2 on the module, addingSerial()
calls to any test that does not have aParallel()
call, and removingParallel()
calls from any test that has it3. - Linters and other tools could start flagging
t.Parallel()
calls as unneeded for modules with ago.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 suite4
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 count5 |
---|---|
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".
Footnotes
-
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. ↩
-
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. ↩
-
This migration is trivially reversible even without version control by adding a
Parallel()
call to any function withoutSerial()
call, and removing allSerial()
calls. ↩ ↩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. ↩ -
Module count obtained on 2025-05-29 by searching on public Github repos using the search query
language:"Go Module" /^go 1\.x/
wherex
is replaced by the go minor version (e.g. 24 for 1.24) ↩