一句话结论
Channel 的阻塞取决于是否有缓冲和对方是否就绪。nil Channel 永久阻塞,已关闭 Channel 写入 panic、读取返回零值。谁写谁关——接收方关闭 Channel 极易 panic。
核心原理
阻塞条件全表
已关闭 Channel 的行为
谁负责关闭 Channel
// ✅ 正确:发送者关闭
func producer(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch) // 发送方关
}
// ❌ 危险:接收者关闭(如果还有其他发送者就会 panic)
func consumer(ch <-chan int) {
for v := range ch {
if someCondition {
// close(ch) ← 编译错误!<-chan 不可关闭
}
}
}
原则: Channel 由唯一的发送方关闭。如果多个发送方共用一个 Channel,需要额外的协调机制(如
sync.Once或专门的doneChannel)。
select + 已关闭 Channel
ch := make(chan int)
close(ch)
select {
case v, ok := <-ch:
// ok=false,v=0。会**立即**匹配这个 case!
// 已关闭 Channel 永远不会阻塞 select
default:
// 永远不会到这里
}
项目中的应用
在 项目二-物联网AI-Agent中枢控制平台 中,通过 select + ctx.Done() 防止 Channel 发送阻塞导致 Goroutine 泄漏:
func (h *DeviceHandler) publishStatus(ctx context.Context, status DeviceStatus) error {
select {
case h.statusCh <- status:
return nil
case <-ctx.Done():
return ctx.Err() // 接收方已退出,优雅放弃
}
}
详见 Golang-并发深度 中的真实泄漏排查案例。
Channel 泄漏
场景 1:发送方泄漏
ch := make(chan int)
go func() {
ch <- 1 // 没人接收 → 永久阻塞 → Goroutine 泄漏
}()
// 忘记从 ch 接收
场景 2:接收方泄漏
go func() {
for v := range ch { // ch 永远不会被 close → 永久阻塞
fmt.Println(v)
}
}()
场景 3:select + nil Channel
var ch chan int // nil
select {
case v := <-ch: // nil Channel 永久阻塞,这个 case 永远不会被选中
case <-time.After(time.Second):
// 超时后走这里
}
// ✅ 利用 nil Channel 的特性:把不想选的 case 置为 nil
技巧:
select中把暂时不想处理的 case 对应的 Channel 设成 nil,该 case 就被禁用(永远不会被选中)。
高频面试问题
Q: 向已关闭 Channel 写入会怎样?从已关闭 Channel 读取呢?
30 秒回答: 写入已关闭 Channel → panic。从已关闭 Channel 读取 → 如果 buf 还有数据返回数据+true;buf 空时返回零值+false(不会 panic,也会立即返回)。
Q: select 中多个 case 同时就绪,选哪个?
30 秒回答: 伪随机选择。Go 运行时对就绪的 case 做均匀洗牌,不是按源码顺序。目的是防止饿死——如果总是按顺序,第一个 case 可能永远霸占。
常见错误
接收者关闭 Channel →
panic: close of closed channel忘记关闭 Channel → range channel 的 Goroutine 永久阻塞
多个发送者共用一个 Channel → 不知道谁来 close
最小实验
// 验证各种 Channel 行为
func main() {
// 1. 无缓冲:发送阻塞直到接收
ch := make(chan int)
go func() { time.Sleep(time.Second); fmt.Println(<-ch) }()
ch <- 42 // 阻塞 1 秒
// 2. 已关闭 Channel 读
close(ch)
v, ok := <-ch
fmt.Println(v, ok) // 0, false
}
速记
无缓冲=同步阻塞;有缓冲=满写阻塞/空读阻塞。nil Chan 永久阻塞。closed Chan:读零值+false,写 panic,再关 panic。谁写谁关。select 已关闭 Chan 不会阻塞。select 多就绪=伪随机。