<p>无缓冲channel的发送和接收必须在不同goroutine中进行,否则必然死锁;错误提示为“fatal error: all goroutines are asleep - deadlock!”,但不指明具体阻塞位置。</p>

无缓冲channel必须配对goroutine收发
无缓冲channel的发送和接收必须发生在不同goroutine,否则必然死锁。运行时报错是fatal error: all goroutines are asleep - deadlock!,但错误位置往往不是你怀疑的那一行——它只告诉你“所有goroutine都卡住了”,不指明谁在等谁。
典型错误写法:
func main() {
ch := make(chan int)
ch <- 42 // 卡在这里,main goroutine阻塞
fmt.Println(<-ch) // 永远执行不到
}修正要点:
- 发送和接收逻辑必须拆到至少两个goroutine中
- 如果用
go func() { ... }()启动接收,注意避免变量捕获问题(比如循环变量) - 不要依赖
time.Sleep来“凑时间差”,那是竞态隐患,不是同步方案
range遍历channel前必须确保它会被关闭
写for v := range ch时,Go会一直等待新数据,直到ch被close()。如果没人关,这个for就永远卡在recv操作上,哪怕缓冲区已空。
立即学习“go语言免费学习笔记(深入)”;
常见误用场景:
- 生产者goroutine提前退出,没调用
close(ch) - 多个生产者,但只关了一部分,或关早了(还有数据没发完)
- 用
sync.WaitGroup协调时,Wait()和close()顺序错乱
安全做法是:由**最后一个完成生产的goroutine**负责关闭。例如:
var wg sync.WaitGroup
go func() {
defer wg.Done()
for _, task := range tasks {
ch <- task
}
}()
wg.Wait()
close(ch) // 所有任务发完才关缓冲channel满后仍会阻塞,不能当队列无限使用
缓冲channel不是“不会阻塞”的保障,只是把阻塞延迟到缓冲耗尽那一刻。一旦len(ch) == cap(ch),下一次或<code>ch 就会像无缓冲一样挂起。
排查时重点关注:
- 消费者处理速度是否明显低于生产者(比如日志写入慢于采集)
- 缓冲容量是否与业务吞吐量匹配(10条缓冲撑不住每秒1000条消息)
- 是否用了
select配合default做非阻塞兜底,还是直接硬塞
示例(避免硬塞):
select {
case ch <- data:
// 成功发送
default:
// 缓冲满,丢弃或降级处理
log.Warn("channel full, drop data")
}多channel协作时容易形成等待环
两个或多个channel交叉收发,极易构成循环等待。比如A等B的数据,B又等A的数据,双方都在上停住,runtime检测为死锁。
典型反模式:
ch1 := make(chan int)
ch2 := make(chan int)
go func() { ch2 <- <-ch1 }() // 等ch1
ch1 <- <-ch2 // 等ch2 → 死锁这类问题在线上常隐藏在函数调用链深处,调试难度高。预防建议:
- 避免在goroutine启动前就对channel做双向依赖
- 用单向channel(
chan / <code>)在函数签名中固化流向,编译期就能拦住反向操作 - 复杂流程优先用状态机+
select控制流转,而不是靠channel接力传递控制权
真正难缠的从来不是报错的死锁,而是那些没触发all goroutines are asleep、却让goroutine越积越多的隐性阻塞——它们往往卡在某个channel操作上,但总有一个goroutine在轮询或sleep,骗过了runtime的检测。查这类问题,得靠pprof抓goroutine堆栈,看哪几行反复出现chan send或chan recv。


















