17370845950

Go测试如何并行执行_Go Parallel测试用法说明
go test -p N 控制测试进程并行数,N 为最大并发进程数(需大于0且不超CPU核心数),每个进程内测试函数仍串行执行;t.Parallel() 则在测试函数级启用goroutine并发,需首行调用且独立于 -p 参数。

Go test -p 参数控制并行度,但不等于测试函数内并发

Go 的 go test 默认串行执行所有测试函数。想让多个 TestXxx 函数同时跑,得用 -p 参数指定最大并行进程数,比如 go test -p 4 表示最多启动 4 个 go test 进程,每个进程仍按顺序跑自己负责的测试函数。这不是「单个测试里开 goroutine」,而是「多个测试函数跨进程并行」。

注意:-p 值不能超过 CPU 核心数(runtime.NumCPU()),超出部分会被截断;它影响的是测试包的构建和执行调度粒度,不是测试逻辑内部行为。

  • -p 1:强制串行,适合调试或有全局状态冲突的测试
  • -p 0:非法,会报错 invalid value "0" for flag -p: must be greater than 0
  • 实际并行数还受 GOMAXPROCS 和系统资源限制,不一定等于你指定的值

t.Parallel() 是测试函数级并发开关,需手动调用

让单个测试函数(如 TestLogin)在运行时与其他测试函数并发执行,必须在函数开头显式调用 t.Parallel()。它不会自动开启,也不依赖 -p —— 即使 -p 1,只要多个测试都调用了 t.Parallel(),它们仍可能被调度到同一进程的不同 goroutine 中并发执行(前提是测试框架认为安全)。

关键规则:

  • 必须在 t.Logt.Error 等任何使用 *testing.T 方法之前调用 t.Parallel()
  • 调用后该测试函数不再独占 t,不能假设执行顺序或共享未加锁的变量
  • 子测试(t.Run)中也可以调用 t.Parallel(),但父测试未调用时,子测试的 Parallel 无效
func TestFetchUser(t *testing.T) {
    t.Parallel() // 必

须放第一行 user, err := FetchUser(123) if err != nil { t.Fatal(err) } if user.Name == "" { t.Fail() } }

并行测试失败时堆栈和日志容易混乱

当多个 t.Parallel() 测试同时写日志或失败时,t.Log 输出可能交错,t.Error 的定位信息也可能指向错误的测试上下文。这不是 bug,是并发执行的自然结果。

缓解方式:

  • 避免在并行测试中依赖全局可变状态(如包级变量、临时文件路径、数据库连接池)
  • t.TempDir() 替代硬编码路径,确保每个测试有独立目录
  • 对共享资源(如 mock server、内存数据库)做隔离或加锁,或改用只读 fixture
  • 调试时临时去掉 t.Parallel(),或用 go test -v -run=^TestName$ 单独跑一个

子测试 + Parallel 组合最常用,但嵌套层级要克制

t.Run 拆分子测试再配合 t.Parallel(),是 Go 中组织数据驱动测试的标准做法。但要注意:只有顶层测试或子测试显式调用 Parallel() 才真正并发;父测试不并发,其所有子测试即使调了 Parallel() 也不会跨父测试并发。

例如:

func TestMathOps(t *testing.T) {
    tests := []struct{
        a, b, want int
    }{{1,2,3}, {2,3,5}}
    for _, tt := range tests {
        tt := tt // 必须重声明,否则闭包捕获循环变量
        t.Run(fmt.Sprintf("Add(%d,%d)", tt.a, tt.b), func(t *testing.T) {
            t.Parallel()
            if got := tt.a + tt.b; got != tt.want {
                t.Errorf("got %d, want %d", got, tt.want)
            }
        })
    }
}

这个例子中,两个子测试会并发执行;但如果把 t.Parallel() 放到外层 TestMathOps 里,效果一样 —— 因为子测试默认继承父测试的并发策略。不过更推荐在子测试里调用,语义更清晰。

别嵌太深:三层以上 t.Run 套用 + Parallel() 容易导致失败定位困难,也增加调度开销。