★ channel 的底层结构是怎样的?收发的基本流程如何?
结论先行
channel 是 goroutine 间通信与同步的原语,底层是一个带内部锁的环形缓冲队列:hchan 里有 buf(有缓冲时)、sendx/recvx 游标、qcount、closed 标志,以及 sendq/recvq 两个等待队列(挂起阻塞的 G)。收发时先尝试“直接对接等待者”或操作缓冲,都不行才把当前 G 挂进等待队列并让出 CPU。
要点
- 结构字段:hchan 大致包含 qcount、dataqsiz、buf、elemsize、closed、sendx、recvx、recvq、sendq、lock。
- 无缓冲与有缓冲:无缓冲要求收发同时在场,天然同步;有缓冲让收发解耦,满了或空了才阻塞。
- 发送流程:加锁 → 若已关闭直接 panic → 有等待的接收者则把数据直接交给对方(不走 buf)→ 否则 buf 有空位就写入 → 再不行把自己封装成 sudog 挂入 sendq 并 gopark 睡眠,等接收方唤醒。
- 接收流程:加锁 → 若 sendq 里有等待的发送者,buf 空则直接对接、buf 非空则先从 buf 取一个并把对方的数据补进 buf → 否则 buf 有数据就取 → 否则挂入 recvq 睡眠。
- 内存模型:channel 收发构成 happens-before:一次发送先于对应的接收完成,它不只是传值,更是同步点。
- nil 与 closed:对 nil channel 收发永久阻塞;从已关闭的 channel 接收立即返回零值;向已关闭 channel 发送会 panic。
- 快慢路径:快路径(对方已在等、缓冲有空位)只涉及一次加锁与内存拷贝;阻塞才涉及挂起与唤醒。
- 唤醒方向:接收方会唤醒 sendq 里一个发送者,发送方会唤醒 recvq 里一个接收者,被唤醒的 G 进入本地队列等待调度。
- 使用提示:需要取消、超时或多路等待时不要只靠 channel 阻塞,配合 select 与 context 使用(见后续题目)。
示例
ch := make(chan int) // 无缓冲:收发必须对接
go func() { ch <- 42 }() // 发送方阻塞直到有人接收
v := <-ch // 对接成功,v == 42
buf := make(chan int, 2) // 有缓冲
buf <- 1
buf <- 2
fmt.Println(<-buf) // 1,缓冲还剩一个
常见追问/记忆点
- 追问:channel 传的是引用还是值?答:按元素类型拷贝值,收发是副本语义;元素是指针时才共享所指对象。
- 追问:channel 为什么比锁慢/快?答:无锁可表达的语义它用锁实现,但避免了锁的临界区滥用;快慢取决于使用方式。
- 追问:缓冲为 1 的 channel 能当锁用吗?答:能(信号量语义),但语义不清晰,互斥场景优先 sync.Mutex。
- 记忆:先对接等待者,再走缓冲,最后挂起自己;hchan 内部自带锁保证并发安全。