应优先使用 errgroup.Group 替代 sync.WaitGroup,因其内置错误传播、上下文取消和自动中断机制;串行步骤显式传递参数,复杂编排用 channel + select 协调分支逻辑。

用 sync.WaitGroup 控制并行任务的生命周期,但别直接裸用
Go 本身没有内置的“工作流引擎”,所以任务编排得靠组合原语。很多人一上来就用 go 启动一堆 goroutine,再用 sync.WaitGroup 等待,结果发现串行逻辑被并行冲垮、错误没传播、上下文取消失效。
关键不是“能不能等”,而是“等谁、什么时候等、失败了怎么办”。比如两个并行分支,其中一个失败,另一个还在跑——你得主动中断它,否则可能浪费资源甚至引发竞态。
-
WaitGroup只负责计数,不处理错误或取消,必须配合context.Context使用 - 不要在 goroutine 内部调用
wg.Done()前做耗时阻塞操作(如无超时的 HTTP 请求),否则WaitGroup可能永远卡住 - 串行步骤之间建议显式传递返回值,而不是靠闭包捕获变量,避免因 goroutine 启动时机导致读到旧值
用 errgroup.Group 替代手写 WaitGroup + error 组合
标准库 golang.org/x/sync/errgroup 是专为“并行执行 + 错误传播 + 上下文取消”设计的,比自己拼 WaitGroup 安全得多。它默认支持 WithContext,第一个出错的任务会自动取消其余正在运行的 goroutine。
常见误区是以为 errgroup 只能用于纯并行——其实它也能支撑串并混合:把串行链打包成一个函数传给 Go,内部再调用并行子任务。
- 串行逻辑写成独立函数,例如
func stepA() error { ... },然后eg.Go(stepA) - 并行分支用
eg.Go并发提交,串行步骤则用普通函数调用,再把结果传给下一步 - 注意
errgroup.Group的零值不可用,必须用errgroup.WithContext(ctx)初始化 - 如果某个并行分支需要返回值,别试图从
eg.Go拿,应该用 channel 或闭包变量收集,且要加锁或用sync.Once
用 channel + select 实现条件分支和串并切换
真正复杂的编排(比如“步骤 B 和 C 并行,都成功后才执行 D;若 B 失败,则跳过 C 直接走 E”)没法靠单一结构搞定,得靠 channel 做信号协调。这时候不要硬套 workflow 库,先用原生机制理清数据流。
典型模式是每个任务输出一个 chan Result,主流程用 select 等待多个 channel,根据收到的消息决定下一步启动哪些任务。channel 关闭本身就能表示“该分支已结束(无论成败)”,比轮询 flag 更可靠。
- 每个任务启动时创建自己的
donechannel,成功/失败都 close 它,主流程用select+case 响应 - 并行分支共用一个
context.Context,由主流程控制取消;串行步骤则各自接收上游传入的 context,并设置自己的超时 - 避免在
select中写死多个case,动态分支要用map[string]chan Result配合循环 select - channel 缓冲区大小设为 1 即可,除非你明确需要缓冲多个结果(比如批量聚合)
别过早引入第三方 workflow 库,先确认是否真需要持久化和重试
像 temporalio/temporal-go 或 uber-go/cadence-client 这类库解决的是长期运行、需故障恢复、跨进程/重启续跑的任务。如果你的“编排”只发生在一次 HTTP 请求生命周期内,或者最长跑几分钟,它们反而增加复杂度:要搭 server、配 client、学 DSL、处理版本兼容。
很多团队踩坑在于,看到“任务编排”四个字就直奔 Temporal,结果发现 90% 的逻辑只是“查 DB → 调 A 接口 → 并行调 B/C → 汇总 → 写日志”,用 errgroup + channel 两页代码就搞定。
- 问自己:任务失败后,是否必须从断点继续?是否要审计每一步执行时间/输入/输出?是否要支持人工干预(跳过某步、重试某步)?
- 如果答案都是“否”,就别碰 Temporal。它的核心价值是状态机持久化,不是简化并发写法
- 即使要用,也别在业务 handler 里直接调
workflow.Execute,先封装一层适配器,隔离 workflow SDK 和业务逻辑
最易被忽略的一点:上下文取消的传递不是自动穿透所有 goroutine 的。哪怕用了 errgroup.WithContext,如果某个子任务内部又启了新 goroutine 却没传 context,那个 goroutine 就收不到取消信号——这种漏网之鱼往往在压测时才暴露。

















