无缓冲 channel 能同步两个 goroutine 是因 send 和 recv 操作必须配对阻塞完成:一方写入即阻塞,直至另一方同时读取,实现“手递手”交接;若仅写入无读取则触发 deadlock。

无缓冲 channel 为什么能同步两个 goroutine
因为 send 和 recv 操作在无缓冲 channel 上是阻塞且配对的:一个 goroutine 写入时会卡住,直到另一个 goroutine 同时读取;反过来也一样。这不是“通知”,而是严格的“手递手”交接——没有中间暂存,必须双方都就位才能继续。
常见错误现象:fatal error: all goroutines are asleep - deadlock,典型就是只写了 ch 却没启动接收方,或接收方启动了但没写 <code>。
- 必须成对出现:一端
ch ,另一端 <code>(或 <code>val := ) - 不能用在单个 goroutine 里自同步(比如先发再收),那会直接死锁
- 如果只是想“等对方做完”,值本身不重要,用
chan struct{}最轻量
用 chan struct{} 实现最简双向等待
当目的只是同步、不传递数据时,struct{} 零大小、零开销,语义也最清晰:它代表“事件发生”,不是“数据到达”。
使用场景:主线程启动 worker goroutine,等它初始化完成后再开始发任务;或者两个 worker 要严格交替执行。
done := make(chan struct{})<br>go func() {<br> // 做一些初始化工作<br> fmt.Println("worker ready")<br> done }()<br><br>fmt.Println("start sending tasks")
- 别用
make(chan bool)或chan int替代——语义不清,还多占内存 - 发送后不关闭 channel 也没关系;但如果接收方可能多次等待,就得考虑是否要关 channel + range 或 select default
- 若需等待多个事件,不要重复用同一个
chan struct{},容易混淆时序
避免在循环里误用无缓冲 channel 导致卡死
新手常把无缓冲 channel 当作“信号灯”反复用,比如在 for 循环里不断 ch ,却只有一个接收方——第一次成功,后面全阻塞。
错误示例:
ch := make(chan int)<br>go func() {<br> for i := 0; i ch }<br>}()<br>for i := 0; i fmt.Println(}
- 根本原因:无缓冲 channel 不保存历史,每次操作都要求实时配对
- 解决方法只有两种:加缓冲(
make(chan int, 1)),或确保发送和接收节奏严格一致(比如用 sync.WaitGroup 更合适) - 如果真要循环通信,优先考虑带缓冲 channel 或用
select+default防卡死
和 sync.WaitGroup 的关键区别在哪
WaitGroup 是“等 N 个 goroutine 结束”,关注的是**生命周期**;无缓冲 channel 是“等某个具体动作发生”,关注的是**事件时序**。它们解决的问题维度不同,不能简单互换。
性能影响:两者都极轻量,但 WaitGroup 是纯原子计数,channel 涉及 goroutine 调度唤醒,有微小额外开销;不过这点差异在绝大多数场景里可忽略。
- 要用
WaitGroup:启动 5 个 worker 并行处理,主线程等全部返回 - 要用无缓冲 channel:A goroutine 必须等 B goroutine 完成某步初始化后才继续,B 也要等 A 发来第一个指令才开始工作
- 混用常见坑:用
WaitGroup等启动,再用 channel 控制后续交互——这没问题;但用 channel 去等“所有 goroutine 结束”,就绕远路且难维护
实际写的时候,最容易被忽略的是:无缓冲 channel 的同步能力完全依赖双方是否真的在运行并执行对应操作。goroutine 被调度延迟、逻辑提前 return、甚至 panic 退出,都会让另一端永远卡住。所以生产环境用它做关键同步点,得搭配超时(select + time.After)或兜底机制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











