无缓冲 channel 在发送或接收时若对方未就绪则一定会阻塞;其本质是同步通信管道,要求发送与接收双方同时就绪才能完成操作。
无缓冲 channel 的本质是同步通信管道:发送和接收必须「碰头」才能完成。只要一方没就绪,另一方立刻阻塞。
ch := make(chan int) 创建后,ch 立即阻塞——因为没有 goroutine 在等接收
val := 同样立即阻塞——因为没人往里发数据,且 channel 未关闭time.Sleep 或同步机制,仍可能因调度顺序导致主 goroutine 先执行发送而死锁典型死锁报错:fatal error: all goroutines are asleep - deadlock! ——这不是 panic,是运行时直接终止,无法 recover。
缓冲区像个小仓库,阻塞只发生在「满」或「空」的临界点,不是一写就卡。
ch := make(chan int, 2) 后,前两次 ch 都不阻塞;第三次才阻塞,直到有人 拿走至少一个值
只在缓冲区为空时阻塞;哪怕刚发过 100 个,只要被消费光了,下一次接收就停住
make(chan int, 0)),不是“非阻塞”,这点常被误读ch := make(chan int, 1)
ch <- 1 // OK:存进缓冲区
ch <- 2 // 阻塞:缓冲区已满
go func() { <-ch }() // 另起 goroutine 消费,释放空间后 ch <- 2 才继续select 本身不阻塞,但它所有 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")
}未初始化的 channel 是 nil,它的收发操作不是报错,而是永久阻塞——连死锁错误都不会触发,goroutine 彻
底“蒸发”。
var ch chan int 声明后未 make,直接 ch 或 → 永久休眠
select 的 default 捕获,调试时极难定位最容易被忽略的一点:channel 阻塞不是 bug,而是 Go 并发模型的设计契约;真正的问题,往往出在「谁该负责接收」「缓冲区是否匹配吞吐节奏」「有没有漏掉关闭或超时」——这些才是实际项目里反复踩坑的地方。