并行测试
来源:17golang原创
时间:2026-11-11 17:10:19 351浏览 收藏
Go 并行测试共用临时目录时,冲突通常来自多个用例写了同一个文件,或父函数用 defer 提前删除了目录。最稳妥的处理是让每个并行子测试调用自己的 t.TempDir(),把临时路径作为参数传给被测函数;确实需要共享时,再明确只读部分、可写部分和清理责任。
下面用两个生成 result.txt 的小用例说明问题,不扩展到通用并发性能优化。
官方文档:https://pkg.go.dev/testing
先把问题缩成两个写文件的子测试
假设 north 和 south 都把结果写进同一个 root/result.txt。即使用例的数据完全不同,只要路径相同,后写入者就可能覆盖前一个用例的数据,读回断言也可能看见对方的内容。这是文件资源共享问题,并不是临时目录名字“看起来相同”就一定有问题。
t.Parallel() 让子测试参与并行调度;它不会复制文件、隔离全局变量或自动替换路径。尤其要注意:并行子测试会等待父测试函数返回后继续运行,因此父函数返回时执行的 defer 不能当作整组子测试结束后的清理。
为每个子测试分配自己的目录
在已有 Go 模块中新建 sandbox_test.go;包名按项目实际情况修改。以下是完整的独立示例,代码未在本文制作过程中执行。
package sandbox
import (
"os"
"path/filepath"
"testing"
)
func TestParallelFiles(t *testing.T) {
cases := []struct {
name string
data string
}{
{name: "north", data: "north-value"},
{name: "south", data: "south-value"},
}
for _, item := range cases {
tc := item // 显式绑定当前用例,兼容旧项目的循环变量语义。
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
dir := t.TempDir() // 每个子测试申请独立目录,并由 testing 管理清理。
path := filepath.Join(dir, "result.txt")
if err := os.WriteFile(path, []byte(tc.data), 0600); err != nil {
t.Fatalf("写文件失败: %v", err)
}
got, err := os.ReadFile(path)
if err != nil {
t.Fatalf("读文件失败: %v", err)
}
// 不依赖兄弟测试的执行顺序,只核对自己写入的数据。
if string(got) != tc.data {
t.Fatalf("内容不匹配: got=%q want=%q", got, tc.data)
}
})
}
}
这里两个文件都叫 result.txt,但所在目录不同。每个子测试只接触自己的路径,所以不需要为文件名本身增加互斥锁。将真实业务接进来时,把 dir 传给文件生成函数,不要让函数偷偷读取包级固定路径。

共享父目录时明确文件归属
有些集成测试需要公共根目录,便于统一管理。根目录可以由父测试创建,但每个写入用例仍应使用独立子目录。下面的代码与前例使用相同的导入项。
func TestSharedRoot(t *testing.T) {
root := t.TempDir() // 父测试拥有公共根目录;不手动提前删除。
for _, name := range []string{"north", "south"} {
caseName := name
t.Run(caseName, func(t *testing.T) {
t.Parallel()
// 两个用例的名字在此固定集合中不同,且只包含简单目录名。
dir := filepath.Join(root, caseName)
if err := os.Mkdir(dir, 0700); err != nil {
t.Fatalf("创建用例目录失败: %v", err)
}
if err := os.WriteFile(filepath.Join(dir, "result.txt"), []byte(caseName), 0600); err != nil {
t.Fatalf("写入失败: %v", err)
}
})
}
}
这个例子只对固定的简单用例名创建子目录。如果用例名来自外部数据,不能直接拿它拼文件路径:重复名、分隔符和上级目录片段都需要另行处理。更简单的选择仍是每个子测试自行申请 TempDir。
| 资源 | 并行用例如何使用 |
|---|---|
| 每个用例的输出文件 | 放入独立目录,互不覆盖 |
| 公共输入样本 | 保持只读,不让用例就地改写 |
| 必须共同修改的同一文件 | 按业务语义协调完整读写操作,或将该组改为串行 |
只给 WriteFile 加锁,而把随后依赖它的 ReadFile 留在锁外,仍可能读到另一个用例的覆盖结果。应先判断两个用例究竟能否独立;隔离输出通常比扩大锁范围更清楚。
把清理绑定到真正的资源拥有者
父测试申请的 TempDir 会保留到它及所有子测试完成,随后自动删除,不需要再写 defer os.RemoveAll(root)。若目录由 os.MkdirTemp 手动创建,则应在拥有它的测试上注册 t.Cleanup,并处理创建和删除错误。
Cleanup 解决的是资源寿命,不负责用例之间的数据隔离。即使目录一直存在,两个并行子测试写同一个文件,仍然会互相影响。反过来,目录完全独立,但清理责任绑定错误,也可能产生文件不存在的失败。

此外,测试函数自己启动的后台 goroutine 必须自行停止并等待结束;TempDir 的自动清理并不意味着任意后台任务都会被等待。否则测试返回后仍在写文件,也会与清理发生冲突。
将示例接入项目并判断结果
先把例子放进已有模块,再将文件写入逻辑替换成项目实际函数。可以使用下面的命令反复运行指定用例;这些是供读者执行的命令,不是本文运行记录。
# 在模块目录中重复运行目标测试,增加观察偶发干扰的机会。 go test -run '^TestParallelFiles$' -count=20 . # 支持竞态检测的环境可补查内存共享;它不能保证发现文件互相覆盖。 go test -race -run '^TestParallelFiles$' -count=5 .
判断重点是每个用例读回自己的内容、错误信息能对应具体子测试,以及项目代码确实使用传入目录。重复运行没有报错只是一次观察,不能代替路径归属分析。降低并行度后暂时不报错,也不能证明共享写入已经安全。
几个容易混淆的问题
同名文件一定冲突吗?
不一定。关键是实际访问路径是否落在同一个文件上。独立 TempDir 内的同名文件可以分别使用。
父测试的 TempDir 能给子测试用吗?
可以。应让父测试承担寿命管理,同时为并行写入划分独立区域;共享目录本身不会自动隔离文件内容。
为什么 race 检测没有提示?
文件内容被其他用例覆盖属于资源层面的逻辑干扰,不一定表现为 Go 内存数据竞争。仍需核对文件路径和操作语义。
有关子测试等待规则,另见 Go 官方说明:https://go.dev/blog/subtests。并行测试中先划清“谁写哪里、谁负责清理”,再决定是否需要共享和同步。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
153 收藏
-
488 收藏
-
250 收藏
-
376 收藏
-
477 收藏
-
222 收藏
-
156 收藏
-
161 收藏
-
223 收藏
-
475 收藏
-
430 收藏
-
216 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习