Channel
Channel 是 Go 的通信原语(CSP):不要通过共享内存通信,通过通信共享内存。面试必考:底层结构、阻塞语义、死锁。
hchan 结构
hchan: 缓冲 + 双向等待队列
- buf:环形缓冲(有缓冲 channel)
- sendq / recvq:阻塞的发送者/接收者队列(双向链表)
- mutex:所有操作加锁(channel 本身并发安全)
- 内存模型:发送和接收有 happens-before 关系(发送完成 → 接收者看到)
缓冲 vs 非缓冲
| 类型 | 语义 | 阻塞 |
|---|---|---|
非缓冲 make(chan int) |
同步:发必须有收 | 发送阻塞直到有人接收,反之亦然 |
缓冲 make(chan int, 3) |
异步:积压 3 个 | 缓冲满才阻塞发送,空才阻塞接收 |
- 非缓冲 channel 是同步点:发和收同时发生(两个 goroutine 会合)
- 缓冲 channel 是队列:解耦生产消费节奏
- 面试点:非缓冲 channel 发完必须有人收,否则死锁(见下)
关闭与取值
close(ch) // 关闭: 不能再发送(发送 panic), 可继续接收
v, ok := <-ch // 关闭且空: ok = false, v 是零值- 关闭原则:只在发送方关闭(接收方关闭会导致发送方 panic)
- 关闭后接收:缓冲里的取完,取到零值 + ok=false(用于广播退出信号)
- 发送到已关闭的 channel 会 panic:关闭和发送的并发要自己保证(谁 close 谁负责)
select:多路等待
select {
case v := <-ch1:
// 处理 ch1
case <-time.After(2 * time.Second):
// 超时
default:
// 非阻塞(所有 case 都不可用)
}- select 同时等待多个 channel,随机选择就绪的 case(多个就绪时随机,防饿死)
- 无 case 就绪且无 default:阻塞等待
- 经典用法:超时控制、退出信号、非阻塞收发
死锁场景(必考)
| 场景 | 原因 |
|---|---|
| 主 goroutine 从无缓冲 channel 读,无人发 | 没有接收者/发送者,全部阻塞 |
| 主 goroutine 向无缓冲 channel 发,无人收 | 同上 |
| 两个 goroutine 互相等对方的 channel | 循环等待 |
ch := make(chan int)
ch <- 1 // fatal error: all goroutines are asleep - deadlock!- 死锁是运行时检测的:所有 goroutine 都阻塞时,runtime 报 deadlock 并崩溃
- 生产避免:明确 channel 方向(
<-chan/chan<-参数约束)、超时兜底、职责清晰
面试追问
- hchan 结构? 环形缓冲 + 发送/接收等待队列 + mutex。操作全加锁所以并发安全
- 缓冲和非缓冲的区别? 非缓冲是同步点(发必须有收),缓冲是队列(满才阻塞)
- 关闭后发送会怎样? panic。关闭后接收:取完缓冲拿零值 + ok=false。只在发送方关闭
- select 随机性? 多个 case 就绪随机选,防一个 case 饿死。配合 default 做非阻塞
- 死锁怎么避免? 明确 channel 方向、超时(select + time.After)、发送接收职责清晰。runtime 检测到全阻塞会报 deadlock