登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

并行测试

来源: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 传给文件生成函数,不要让函数偷偷读取包级固定路径。

north与south子测试分别拥有目录和结果文件
图1:独立目录结构说明图。两个用例的同名文件属于不同目录,连线表示资源归属,不表示执行顺序。

共享父目录时明确文件归属

有些集成测试需要公共根目录,便于统一管理。根目录可以由父测试创建,但每个写入用例仍应使用独立子目录。下面的代码与前例使用相同的导入项。

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 解决的是资源寿命,不负责用例之间的数据隔离。即使目录一直存在,两个并行子测试写同一个文件,仍然会互相影响。反过来,目录完全独立,但清理责任绑定错误,也可能产生文件不存在的失败。

父测试管理共享根目录和清理责任,子测试分别拥有写入区域
图2:资源拥有者结构说明图。父测试负责公共根目录的寿命,north 和 south 分别负责自己的输出区域;清理关系不等于写入同步。

此外,测试函数自己启动的后台 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。并行测试中先划清“谁写哪里、谁负责清理”,再决定是否需要共享和同步。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>