Golang channel阻塞行为的触发条件

无缓冲 channel 在发送或接收时若对方未就绪则一定会阻塞;其本质是同步通信管道,要求发送与接收双方同时就绪才能完成操作。

无缓冲 channel 什么时候一定会阻塞?

无缓冲 channel 的本质是同步通信管道:发送和接收必须「碰头」才能完成。只要一方没就绪,另一方立刻阻塞。

  • ch := make(chan int) 创建后,ch 立即阻塞——因为没有 goroutine 在等接收
  • val := 同样立即阻塞——因为没人往里发数据,且 channel 未关闭
  • 哪怕只差毫秒,比如先启 goroutine 再发数据,但没加 time.Sleep 或同步机制,仍可能因调度顺序导致主 goroutine 先执行发送而死锁

典型死锁报错:fatal error: all goroutines are asleep - deadlock! ——这不是 panic,是运行时直接终止,无法 recover。

有缓冲 channel 的阻塞边界在哪?

缓冲区像个小仓库,阻塞只发生在「满」或「空」的临界点,不是一写就卡。

  • 创建 ch := make(chan int, 2) 后,前两次 ch 都不阻塞;第三次才阻塞,直到有人 拿走至少一个值
  • 接收方 只在缓冲区为空时阻塞;哪怕刚发过 100 个,只要被消费光了,下一次接收就停住
  • 注意:缓冲区大小为 0 等价于无缓冲(make(chan int, 0)),不是“非阻塞”,这点常被误读
ch := make(chan int, 1)
ch <- 1        // OK:存进缓冲区
ch <- 2        // 阻塞:缓冲区已满
go func() { <-ch }() // 另起 goroutine 消费,释放空间后 ch <- 2 才继续

select 语句里的阻塞陷阱

select 本身不阻塞,但它所有 case 都不可达时,就会整体挂起——这是最隐蔽的阻塞来源之一。

  • 所有 channel 都空(接收 case)、都满(发送 case),且没写 default → 整个 goroutine 卡死
  • 写了 default 就是非阻塞轮询,但要注意:它不表示“失败”,而是“此刻不可行”,需自行判断是否重试或放弃
  • select 中混用超时(time.After)和 channel 操作,是避免无限等待的标准做法
select {
case msg := <-ch:
    fmt.Println("got", msg)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

nil channel 的特殊阻塞行为

未初始化的 channel 是 nil,它的收发操作不是报错,而是永久阻塞——连死锁错误都不会触发,goroutine 彻底“蒸发”。

  • var ch chan int 声明后未 make,直接 ch 或 → 永久休眠
  • 这种阻塞不会 panic,也不会被 selectdefault 捕获,调试时极难定位
  • 常见于条件分支中 channel 初始化遗漏,或函数参数传入了零值 channel

最容易被忽略的一点:channel 阻塞不是 bug,而是 Go 并发模型的设计契约;真正的问题,往往出在「谁该负责接收」「缓冲区是否匹配吞吐节奏」「有没有漏掉关闭或超时」——这些才是实际项目里反复踩坑的地方。