
在 Go 中,使用一个缓冲通道配合多个读取协程时,若未正确关闭通道并处理接收终止逻辑,极易触发“all goroutines are asleep”死锁错误;关键在于确保发送方关闭通道的时机早于 wg.Wait(),且所有读者能及时感知关闭状态并主动退出。
在 go 中,使用一个缓冲通道配合多个读取协程时,若未正确关闭通道并处理接收终止逻辑,极易触发“all goroutines are asleep”死锁错误;关键在于确保发送方关闭通道的时机早于 `wg.wait()`,且所有读者能及时感知关闭状态并主动退出。
要实现一个缓冲通道(buffered channel)被多个 goroutine 安全并发读取,核心挑战在于:通道关闭时机、读者退出机制与同步等待的协调。原代码中死锁的根本原因是:wg.Wait() 在 close(cs) 之前被调用,而所有 reader 协程仍在阻塞等待从已无数据且未关闭的通道中接收值——此时 sender 已退出,通道既未关闭又无新数据,所有 reader 永久阻塞,wg.Wait() 永不返回。
✅ 正确做法:发送方负责关闭通道,并确保读者能优雅退出
- 关闭通道必须由 sender 发起,且在发送完毕后立即执行(推荐用 defer close(cs));
- 每个 reader 必须通过 x, ok := <-cs 检查通道是否已关闭(ok == false),并在首次接收失败时调用 wg.Done() 并退出;
- close(cs) 必须发生在 wg.Wait() 之前,但不能放在 main 函数末尾——因为此时 reader 可能尚未开始读取或仍在等待,导致 wg.Wait() 提前阻塞。
以下是修复后的完整示例:
package main
import (
"fmt"
"strconv"
"sync"
)
func sender(wg *sync.WaitGroup, cs chan int) {
defer wg.Done()
defer close(cs) // ✅ 关键:sender 发送完毕后立即关闭通道
for i := 0; i < 2; i++ {
fmt.Println("Sending:", i)
cs <- i
}
}
func receiver(wg *sync.WaitGroup, cs chan int) {
defer wg.Done() // ✅ 确保无论是否读到数据,reader 都会通知 WaitGroup
for {
x, ok := <-cs
if !ok {
return // ✅ 通道已关闭,reader 退出
}
fmt.Println("Retrieved:", strconv.Itoa(x))
}
}
func main() {
wg := &sync.WaitGroup{}
cs := make(chan int, 1000) // 缓冲容量足够容纳全部发送数据
wg.Add(1)
go sender(wg, cs)
const numReceivers = 30
for i := 0; i < numReceivers; i++ { // ✅ 修正循环起始为 0,更符合习惯
wg.Add(1)
go receiver(wg, cs)
}
wg.Wait() // ✅ 此时 sender 已关闭通道,所有 reader 均已退出
fmt.Println("All done.")
}⚠️ 关键注意事项
- 不要在 main 中手动 close(cs):这会导致竞态——若 close 执行过早(如在 sender 启动前),reader 可能立即收到 ok == false 而提前退出,造成逻辑遗漏;若过晚(如 wg.Wait() 之后),则必然死锁。
- 每个 reader 的 wg.Done() 必须保证执行:使用 defer wg.Done() 是最稳妥的方式,避免因 return 或 panic 导致计数遗漏。
- 缓冲通道 ≠ 多生产者/多消费者队列:本方案适用于单生产者、多消费者共享只读数据流场景;若需多个 sender,需额外加锁或改用 sync.Pool / chan struct{} 信号协调。
- 性能提示:缓冲大小应合理预估(如本例 1000 远超实际发送量 2),过大浪费内存,过小可能导致 sender 阻塞(虽本例无此问题,但通用设计需考虑)。
通过以上调整,即可安全、高效地让多个 reader 并发消费同一缓冲通道,彻底规避死锁风险。

















