
本文介绍一种基于 fan-in 模式的 go 并发测试调度方案,通过预处理器(prepper)和校验器(validator)双工作池协同工作,确保测试按“准备→校验→递归执行子测试”严格顺序执行,同时充分利用并发能力。
在构建高可靠性的集成测试框架时,单纯的并行执行往往破坏逻辑依赖——例如某测试必须先完成资源初始化(Prep),再反复校验就绪状态(Validate),最后才可启动其子测试。本文提出的 ConcTest 框架正是为解决这一问题而生:它不追求“所有测试完全并行”,而是将阶段内并发 + 阶段间串行作为核心范式,用简洁的通道与 goroutine 编排实现可伸缩、可嵌套、可中断的测试流。
核心设计:双通道 Fan-in + 结果回传结构
框架摒弃了原始 TestSuite 中裸函数通道的设计,转而引入两个专用传输结构体:
type prepperTransport struct {
Prepper Prepper
Result chan PrepperResult // 单次结果回传通道
}
type validatorTransport struct {
Validator Validator
Result chan ValidatorResult
}这种“函数 + 回传通道”的封装,是实现 Fan-in(多生产者 → 单消费者池)+ 同步等待(Caller 等待专属结果) 的关键。每个测试调用 ct.prepperChan
工作池启动与生命周期管理
Run() 方法中,预处理器与校验器工作池被独立启动:
for i := 0; i < ct.ConcurrentPreppers; i++ {
go func() {
for p := range ct.prepperChan {
p.Result <- p.Prepper() // 同步执行 Prep,结果直送 caller 通道
}
}()
}注意:此处未使用 time.Sleep(time.Second)(原示例仅为演示延迟),生产环境应移除或替换为真实耗时操作。工作池 goroutine 持续监听对应通道,无锁、无共享变量,符合 Go 的 CSP 哲学。
测试执行流程:递归 + WaitGroup 精确同步
runTest() 是整个调度的引擎,其流程清晰分层:
- Prep 阶段:发送 prepperTransport → 等待结果 → 失败则终止当前测试;
- Validate 阶段:循环发送 validatorTransport,每次校验失败后按 Frequency 休眠,直至 Pass == true 或达 MaxRuns;
- Children 阶段:仅当父测试通过后,才为每个子测试 Add(1) 并 go ct.runTest(c),由 sync.WaitGroup 统一协调生命周期。
该设计天然支持深度嵌套的测试树,且任意节点失败均不会阻塞其他分支执行——因为每个 go ct.runTest(c) 独立受控于自己的 WaitGroup 计数。
注意事项与优化建议
- ✅ 通道容量:prepperChan 和 validatorChan 应设为带缓冲通道(如 make(chan *prepperTransport, 1024)),防止高并发下发送方阻塞;
- ⚠️ 错误传播:PrepperResult 当前定义为 error 类型,但若需携带更多上下文(如重试次数、超时时间),建议扩展为结构体;
- ? 资源清理:实际 Prep/Validate 可能涉及数据库连接、临时文件等,应在 Prepper/Validator 函数内完成 defer 清理,而非依赖外部回收;
- ? 可观测性增强:可在 runTest 开头添加 log.Printf("[START] %s", t.Convey),并在各阶段插入耗时统计(time.Since(start)),便于性能分析;
- ? 取消支持(进阶):如需支持全局中断(如 Ctrl+C),可向 ConcTest 添加 ctx context.Context 字段,并在所有 select 中加入 ctx.Done() 分支。
此方案证明:在 Go 中实现复杂依赖的并发调度,无需引入重型调度器或状态机;只需合理组合 channel、goroutine 与 sync.WaitGroup,即可构建出清晰、健壮、易于演化的测试基础设施。










