必须用make初始化channel才能发送或接收,nil channel操作会panic;无缓冲channel(make(chan T)或make(chan T, 0))同步阻塞,缓冲channel异步通信,容量不能为负。

channel 必须初始化才能发送或接收
声明一个 chan int 变量只是创建了 nil channel,对它做 send 或 recv 会立即 panic:"panic: send on nil channel" 或 "fatal error: all goroutines are asleep - deadlock"。必须用 make 初始化,并指定缓冲区大小(0 表示无缓冲)。
常见写法:
ch := make(chan int) // 无缓冲,同步通信 ch := make(chan string, 10) // 缓冲容量为 10,异步通信
注意:make(chan T) 和 make(chan T, 0) 效果相同,但后者语义更明确;缓冲区大小不能为负,传负数会 panic。
无缓冲 channel 的阻塞行为决定协程协作节奏
无缓冲 channel 的 操作是同步的:发送方会阻塞,直到有接收方就绪;接收方也一样。这天然适合“等待完成”“配对协作”场景,比如启动 goroutine 后等它结束:
done := make(chan bool)
go func() {
// 做一些工作
time.Sleep(100 * time.Millisecond)
done <- true // 发送完成信号
}()
<-done // 主 goroutine 阻塞在此,直到收到信号容易踩的坑:
- 在同一个 goroutine 中对无缓冲 channel 先发后收(或先收后发),必然死锁
- 多个 goroutine 同时向一个无缓冲 channel 发送,但只有一个接收者,其余发送者永久阻塞
- 忘记关闭 channel 导致 range 无限等待(见下一条)
range channel 要求 channel 关闭,否则永远阻塞
for v := range ch 本质是持续接收,直到 channel 关闭且缓冲区为空。如果没人调用 close(ch),循环永远不会退出——即使所有发送者已退出,只要 channel 没关,range 就卡住。
典型模式是“扇出-扇入”(fan-out/fan-in):
func fanIn(chs ...<-chan int) <-chan int {
out := make(chan int)
for _, ch := range chs {
go func(c <-chan int) {
for v := range c { // 这里依赖每个 c 被关闭
out <- v
}
}(ch)
}
go func() {
close(out) // 所有子 goroutine 结束后才关 out
}()
return out
}关键点:
- 只有 sender 应该调用
close(),receiver 调用会 panic -
close()只能调用一次,重复 close panic - 关闭后仍可接收(返回零值+ok=false),但不可再发送
select + timeout 是避免 channel 永久阻塞的标准解法
生产环境几乎不会让 goroutine 在 channel 上无限等待。用 select 配合 time.After 或 context.WithTimeout 实现超时控制:
select {
case v := <-ch:
fmt.Println("received:", v)
case <-time.After(2 * time.Second):
fmt.Println("timeout")
}注意:
-
select默认随机选择一个就绪 case;所有 channel 都未就绪时,若含default则立即执行,否则阻塞 -
time.After创建新 timer,频繁调用可能泄漏;高频率场景建议复用time.NewTimer - 涉及 context 时,优先用
替代time.After,便于主动取消
channel 的核心不是“传递数据”,而是“协调执行时机”。理解阻塞点在哪、谁负责关闭、如何设防超时,比记住语法更重要。











