
本文讲解如何在 Go 中启动多个 goroutine 并发执行任务(如调用外部进程),同时实时消费返回结果,避免死锁,并在所有任务完成后优雅退出程序。核心在于结合 sync.WaitGroup 与通道关闭机制。
本文讲解如何在 go 中启动多个 goroutine 并发执行任务(如调用外部进程),同时实时消费返回结果,避免死锁,并在所有任务完成后优雅退出程序。核心在于结合 `sync.waitgroup` 与通道关闭机制。
在 Go 并发编程中,常见的误区是:向无缓冲通道发送数据后,未显式关闭通道,却试图通过 for range 持续读取——这会导致主 goroutine 永久阻塞,最终触发 fatal error: all goroutines are asleep - deadlock!。问题中的代码正是如此:results 通道从未被关闭,而 for result := range results 会一直等待新值,即使所有 execTask 已执行完毕。
正确的解法是引入同步原语 sync.WaitGroup 来跟踪 goroutine 的完成状态,并在所有任务结束后主动关闭通道,从而让 range 循环自然终止。以下是推荐实现:
func main() {
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 等待全部完成并关闭通道
go func() {
wg.Wait()
close(results) // 关键:通知消费者“不会再有新数据”
}()
// 实时消费结果 —— 此处可立即处理每个返回值,无需等待全部完成
for result := range results {
fmt.Println("Received:", result)
// ✅ 可在此处启动后续处理(如写入文件、聚合统计、触发回调等)
// 例如:go processResult(result)
}
// 所有结果处理完毕,程序自然退出
}
⚠️ 关键注意事项:
-
必须关闭通道:
close(results)是for range退出的唯一信号;仅靠wg.Wait()不足以解除range阻塞。 -
避免变量捕获陷阱:循环中启动 goroutine 时,需将
task作为参数传入匿名函数(如func(t int)),否则所有 goroutine 可能共享同一个task变量值。 -
建议使用带缓冲通道:
make(chan int, len(tasks))可防止execTask在发送结果时因消费者尚未开始读取而阻塞(尤其当任务执行较快时)。 -
无需额外同步:
range对已关闭通道的安全读取是 Go 内置保障,不会 panic;且close()只能由写端调用一次,因此放在wg.Wait()后是线程安全的。
该模式完美契合“边执行边处理”的需求:execTask 完成即刻发送结果,主 goroutine 在 range 中即时响应;所有任务终结后,通道关闭,循环结束,程序干净退出。这是 Go 生产环境中处理并发 I/O(如批量 HTTP 请求、进程调用、数据库查询)的标准范式。










