学习目标
- 理解 channel 在 Go 并发模型中的定位
- 掌握无缓冲 channel 与有缓冲 channel 的行为差异
- 搞清楚 channel 的阻塞语义
- 避免 close、读写 channel 的常见错误
一、为什么 Go 需要 channel
在 Go 中,并发并不是靠“共享内存 + 锁”作为第一选择,而是:
通过通信来共享数据,而不是通过共享数据来通信
channel 正是 Go 并发模型中的核心通信机制。
channel 解决的不是“并发执行”,而是:
- goroutine 之间的数据传递
- goroutine 之间的同步与协作
工程认知:goroutine 负责“并发执行”,channel 负责“安全协作”。
二、channel 的基本定义与使用
定义一个 channel:
ch := make(chan int)发送数据:
ch <- 10接收数据:
v := <-chchannel 是强类型的:
chan int只能传int- 编译期就能发现类型错误
三、无缓冲 channel(同步语义)
无缓冲 channel:
ch := make(chan int)行为规则:
- 发送操作会阻塞,直到有接收者
- 接收操作会阻塞,直到有发送者
示例:
func main() {
ch := make(chan int)
go func() {
ch <- 1
fmt.Println("send done")
}()
v := <-ch
fmt.Println("recv:", v)
}输出顺序一定是:
send done
recv: 1结论:无缓冲 channel 天然具备“同步点”的语义。
四、有缓冲 channel(异步语义)
定义有缓冲 channel:
ch := make(chan int, 2)行为规则:
- 缓冲未满:发送不阻塞
- 缓冲已满:发送阻塞
- 缓冲非空:接收不阻塞
- 缓冲为空:接收阻塞
示例:
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
fmt.Println("send finished")
fmt.Println(<-ch)
fmt.Println(<-ch)
}说明:
- 前两个 send 不会阻塞
- 第三个 send 会阻塞(如果存在)
五、channel 的阻塞本质
channel 的阻塞并不是“CPU 忙等”,而是:
- goroutine 被 runtime 挂起
- 让出执行权给其他 goroutine
- 条件满足后被重新唤醒
工程结论:channel 阻塞是高效的,但逻辑阻塞依然会导致 goroutine 泄漏。
六、channel 的关闭(close)语义
关闭 channel:
close(ch)核心规则(必须记住):
- 只能关闭发送方负责的 channel
- 关闭一个已经关闭的 channel 会 panic
- 向已关闭的 channel 发送数据会 panic
- 从已关闭的 channel 接收数据是安全的
七、从关闭的 channel 接收数据
示例:
v, ok := <-ch含义:
ok == true:正常接收到数据ok == false:channel 已关闭且缓冲已空
常见用法:
for v := range ch {
fmt.Println(v)
}说明:
range会一直读,直到 channel 被关闭- 这是消费者模型中最常见的写法
八、谁来关闭 channel(工程共识)
规则:
- 发送方关闭 channel
- 接收方不关闭 channel
- 多个发送方时,通常由“协调者”关闭
错误示例:
close(ch) // 接收方关闭,极易 panic工程结论:channel 的关闭是一种“广播信号”,不是资源释放。
九、channel 的常见错误总结
- 向 nil channel 发送或接收(永久阻塞)
- 忘记关闭 channel,导致 goroutine 泄漏
- 重复关闭 channel(panic)
- 多个发送方随意 close channel
- 用 channel 当普通队列,不考虑阻塞语义
十、channel 的工程定位总结
channel 适合:
- goroutine 之间的通信
- 任务协作、流水线
- 信号通知(完成、退出、取消)
channel 不适合:
- 高频随机读写的大量共享数据
- 复杂对象状态管理(更适合 mutex)
一句话总结:
channel 是用来“协作”的,不是用来“存数据”的。
预告:Day 13|select 与多路复用
下一天将系统讲解:
- select 的执行规则
- 多 channel 协调
- 超时与 default 分支
- 如何避免 channel 死锁