
本文讲解如何使用 sync.waitgroup 配合 channel 实现:在并发启动多个 goroutine(如执行外部命令)后,不阻塞地实时消费返回结果,并在全部任务结束后安全退出程序,避免死锁。
本文讲解如何使用 sync.waitgroup 配合 channel 实现:在并发启动多个 goroutine(如执行外部命令)后,不阻塞地实时消费返回结果,并在全部任务结束后安全退出程序,避免死锁。
在 Go 并发编程中,一个常见误区是:向无缓冲 channel 发送数据后,未显式关闭 channel 就直接用 for range 循环接收——这会导致主 goroutine 永久阻塞,引发 fatal error: all goroutines are asleep - deadlock!。根本原因在于:range 会持续等待新值,直到 channel 被关闭;而若无人调用 close(),且所有写入 goroutine 已退出,程序便陷入死锁。
正确的解法是分离“任务协调”与“结果消费”逻辑:用 sync.WaitGroup 跟踪所有 goroutine 的完成状态,并在最后统一关闭结果 channel。以下是推荐实现:
package main
import (
"fmt"
"sync"
)
func execTask(input int, results chan<- int) {
// 模拟耗时操作(例如:os/exec.Command().Run())
// 实际中可在此处捕获错误、解析 stdout 等
result := input * input // 示例计算
results <- result
}
func main() {
tasks := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
results := make(chan int, len(tasks)) // 可选:加缓冲避免 sender 阻塞
// 启动所有任务 goroutine
wg.Add(len(tasks))
for _, task := range tasks {
go func(t int) {
defer wg.Done()
execTask(t, results)
}(task)
}
// 单独 goroutine 等待全部任务结束并关闭 channel
go func() {
wg.Wait()
close(results) // 关键:通知 range 循环可以终止
}()
// 实时消费结果 —— 此循环与任务执行并发进行
for result := range results {
fmt.Printf("Received result: %d\n", result)
// ✅ 此处可立即处理每个结果:写入文件、更新数据库、触发下游任务等
// 甚至可再启 goroutine 进行异步后续处理
}
fmt.Println("All tasks completed. Exiting.")
}关键要点说明:
-
sync.WaitGroup是轻量级同步原语,通过Add()、Done()和Wait()精确控制任务生命周期; -
close(results)必须由单个 goroutine 执行(本例中是匿名 goroutine),避免多处关闭 panic; - channel 缓冲(如
make(chan int, len(tasks)))非必需,但能防止 sender 在 receiver 尚未启动时阻塞(尤其当execTask极快时); -
for range results天然支持“边收边处理”,无需额外同步——Go 的 channel 保证了线程安全的数据传递; - 若需在处理结果时捕获错误或区分成功/失败,建议将
results改为结构体 channel(如chan struct{ Value int; Err error }),并在execTask中统一发送。
此模式广泛适用于批处理外部命令、HTTP 请求聚合、分布式任务分发等场景,兼顾实时性与终态可控性。

















