Go 子测试调用 Parallel 后父测试什么时候继续执行
来源:17golang原创
时间:2026-10-06 13:47:38 420浏览 收藏
子测试调用 t.Parallel() 后,直接包住它的那次 t.Run 就可以解除阻塞,父测试函数会继续执行后面的代码。此时子测试在 t.Parallel 之后的部分处于暂停状态,通常要等父测试函数返回后才进入并行阶段;不过父测试整体还没有结束,它仍会等待所有子测试完成。
官方地址:https://pkg.go.dev/testing#T.Run
t.Run返回:子测试函数返回,或者子测试调用了t.Parallel。- 父测试函数返回:父函数正文和普通
defer都走完,并行子测试才有机会恢复。 - 父测试完成:父函数已返回,而且它的全部子测试也都完成。
我最初混淆的是 Run 返回和测试完成
第一次读并行子测试时,我很容易把“t.Run 已经返回”理解成“子测试已经跑完”。真正的分界线是:T.Run 会在子测试函数返回,或者子测试调用 t.Parallel 变成并行测试时解除阻塞。后一个条件只表示子测试交出了当前执行位置,不表示它已经完成。
Go 官方还明确说明,父测试只有在全部子测试完成后才算完成。因此父代码会继续、子代码会暂停、父测试会等待,这三个状态可以同时成立。

用最小测试标出三个位置
下面的代码不依赖精确日志时间,只用三个位置帮助判断控制权在哪一侧。子测试先执行到 t.Parallel,随后直接 t.Run 可以返回,父函数继续;子测试中 t.Parallel 后面的断言则要等到父测试函数返回后再恢复。
package parallelparent
import "testing"
func TestParentContinues(t *testing.T) {
t.Log("父测试:调用 Run 之前")
t.Run("child", func(t *testing.T) {
// 这行位于 Parallel 之前,会在 Run 解除阻塞前执行。
t.Log("子测试:调用 Parallel 之前")
t.Parallel()
// 这部分先暂停,等父测试函数返回后再进入并行阶段。
t.Log("子测试:Parallel 之后")
})
// 子测试调用 Parallel 后,父测试可以继续到这里。
t.Log("父测试:Run 已返回,但子测试未必完成")
}
这里最有用的判断不是猜日志会不会紧挨着出现,而是看代码位置:Run 后的父代码不会等待并行子测试的后半段;整个 TestParentContinues 却会等子测试全部完成后才报告最终结果。
普通 defer 可能比并行子测试更早清理资源
这个语义最容易在共享资源上踩坑。父函数返回时会执行它自己的普通 defer,而直接子测试在 t.Parallel 之后还没有恢复。如果父函数用 defer 关闭数据库、HTTP 测试服务或临时依赖,子测试恢复后可能面对已经被关闭的资源。
我更倾向于把“测试树结束后再清理”的动作交给 t.Cleanup。官方契约是:Cleanup 注册的函数会在当前测试及其全部子测试完成后调用。
package parallelparent
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestSharedServer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 测试服务只返回稳定状态,避免共享可变业务数据。
w.WriteHeader(http.StatusNoContent)
}))
// Cleanup 会等当前测试及全部子测试完成后再关闭服务。
t.Cleanup(server.Close)
for _, name := range []string{"first", "second"} {
name := name // 兼容旧版本 Go 的循环变量捕获方式。
t.Run(name, func(t *testing.T) {
t.Parallel()
// 每个子测试创建自己的请求,只有服务地址被共享。
req, err := http.NewRequest(http.MethodGet, server.URL, nil)
if err != nil {
t.Fatal(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close() // 响应体只属于当前子测试。
if resp.StatusCode != http.StatusNoContent {
t.Fatalf("状态码 = %d,期望 %d", resp.StatusCode, http.StatusNoContent)
}
})
}
}

需要明确等待一组子测试时,加一层顺序分组
有时清理动作必须写在一段普通代码里,而不是注册成 Cleanup。此时可以增加一个不调用 t.Parallel 的分组子测试,让并行测试成为它的子节点。分组函数返回后,组内并行子测试开始恢复;外层那次 t.Run("parallel-group", ...) 要等组内子测试全部完成才返回,所以后面的清理代码拥有清楚的等待边界。
package parallelparent
import "testing"
func TestGroupedWorkers(t *testing.T) {
resource := openResourceForTest()
t.Run("parallel-group", func(t *testing.T) {
for _, input := range []string{"a", "b", "c"} {
input := input // 为每个子测试固定当前输入。
t.Run(input, func(t *testing.T) {
t.Parallel()
// 组内子测试并行使用同一个只读测试资源。
if err := resource.Check(input); err != nil {
t.Fatal(err)
}
})
}
})
// 分组 Run 返回时,组内并行子测试已经全部完成。
resource.Close()
}
// testResource 仅表示示例中的受控测试资源。
type testResource struct{}
func openResourceForTest() *testResource { return &testResource{} }
func (r *testResource) Check(input string) error {
// 示例只展示父子测试边界,不引入额外业务逻辑。
return nil
}
func (r *testResource) Close() {
// 实际项目可在这里释放连接、文件或临时服务。
}
这个分组方式的价值在于局部:它只等待当前组里的并行子测试,不要求把整个顶层测试都改成串行清理脚本。
表驱动并行测试里我会保留的边界
- Parallel 之前只做局部准备。 不要在这里修改其他并行子测试也会读写的全局状态。
- 共享资源用并发安全或只读设计。
t.Parallel解决的是调度,不会自动给业务对象加锁。 - 测试树级清理优先用 t.Cleanup。 普通
defer只绑定当前函数返回。 - 需要等待一组子测试时用顺序分组。 让外层 Run 的返回成为清楚的组完成边界。
- -parallel 只限制并行测试数量。 它不会改变父子测试的等待语义,也不保证具体调度顺序。
验收时只问这四个问题
| 检查点 | 正确判断 |
|---|---|
| 直接 t.Run 何时返回 | 子函数返回,或子测试调用 t.Parallel |
| Parallel 后的子代码何时恢复 | 父测试函数返回、非并行阶段结束后 |
| 父测试何时算完成 | 父函数返回且全部子测试完成后 |
| 共享资源何时清理 | 用 t.Cleanup 等到测试树完成,或在分组 Run 返回后清理 |
常见问题
父测试在 t.Run 返回后能立刻读取子测试的最终结果吗? 不能这样假设。子测试可能只执行到 t.Parallel 就暂停,后半段尚未完成。
父测试函数返回后,父测试是不是就结束了? 不是。父测试整体还要等待所有子测试完成。
父测试里的 defer 适合关闭并行子测试共用的服务吗? 通常不适合,因为 defer 随父函数返回执行,可能早于并行子测试恢复。优先使用 t.Cleanup 或顺序分组。
把顶层测试也调用 t.Parallel 能解决等待问题吗? 不能。它会改变顶层测试与其他并行测试的关系,但不会取消父测试等待子测试完成的规则。
-
377 收藏
-
275 收藏
-
467 收藏
-
485 收藏
-
411 收藏
-
400 收藏
-
497 收藏
-
267 收藏
-
133 收藏
-
130 收藏
-
Golang · Go问答 | 3小时前 | go · TLS · 网络安全 · VerifyConnection Go InsecureSkipVerify x509.Verify TLS证书校验 证书固定384 收藏
-
468 收藏
-
223 收藏
-
412 收藏
-
287 收藏
-
290 收藏
-
479 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习