Go 专家(06):channel 底层实现 —— hchan 结构、环形缓冲区、发送/接收队列
更新时间:2026-09-01。本文是
languages/go/expert/专家层第 06 篇,接 race detector。channel 是 Go 并发编程的核心。底层是一个 hchan 结构体,包含环形缓冲区、发送/接收 goroutine 队列、互斥锁。理解 channel 底层,才能真正理解"不要通过共享内存来通信,而要通过通信来共享内存"。
本文要回答的问题
hchan结构体包含哪些字段?- 有缓冲和无缓冲 channel 在底层有什么区别?
- 发送和接收的 goroutine 队列是怎么组织的?
- 非阻塞发送/接收(select default)怎么实现?
一、hchan 结构体
type hchan struct {
qcount uint // 当前元素个数
dataqsiz uint // 环形缓冲区大小
buf unsafe.Pointer // 指向环形缓冲区
elemsize uint16 // 元素大小
closed uint32 // 关闭标志
elemtype *_type // 元素类型
sendx uint // 发送索引
recvx uint // 接收索引
recvq waitq // 接收 goroutine 队列
sendq waitq // 发送 goroutine 队列
lock mutex // 互斥锁,保护 hchan 的所有字段
}有缓冲 channel(hchan):
┌──────────────────────────────────┐
│ buf (环形缓冲区) │
│ ┌────┬────┬────┬────┬────┬────┐ │
│ │ │ │ │ │ │ │ │
│ └────┴────┴────┴────┴────┴────┘ │
│ ↑ recvx ↑ sendx │
│ qcount = 3, dataqsiz = 6 │
├──────────────────────────────────┤
│ recvq(接收 goroutine 队列) │
│ goroutine A → goroutine B → nil │
│ sendq(发送 goroutine 队列) │
│ goroutine C → nil │
├──────────────────────────────────┤
│ lock(互斥锁) │
│ closed(关闭标志) │
└──────────────────────────────────┘二、有缓冲 channel 发送/接收流程
发送流程(ch <- x):
- 加锁(
hchan.lock) - 如果有 recvq 有 goroutine 在等 → 直接发送给第一个等待的 goroutine(绕过缓冲区)
- 如果缓冲区没满 → 写入 buf[sendx],sendx++,qcount++
- 如果缓冲区满了 → 当前 goroutine 入队 sendq,等待被唤醒
- 解锁
接收流程(<-ch):
- 加锁
- 如果有 sendq 有 goroutine 在等 → 从 sendq 取第一个 goroutine,直接读它的数据
- 如果缓冲区有数据 → 从 buf[recvx] 读,recvx++,qcount--
- 如果缓冲区空且没有发送者排着 → 当前 goroutine 入队 recvq,等待
- 解锁
关键设计: 发送和接收尽量直接传递,不经过缓冲区,减少拷贝。
三、无缓冲 channel 发送/接收流程
无缓冲的 dataqsiz = 0,就没有 buf:
发送(ch <- x):
- 加锁
- 如果 recvq 有 goroutine → 直接发送给它,两个 goroutine 同时唤醒
- 如果 recvq 为空 → 排队,阻塞等待
接收(<-ch):
- 加锁
- 如果 sendq 有 goroutine → 直接接收
- 如果 sendq 为空 → 排队,阻塞等待
无缓冲 channel 的发送和接收总是同步的——要么发送者等接收者,要么接收者等发送者,两者必须同时准备好。
四、非阻塞 channel:select default
select {
case ch <- x:
// 发送成功
default:
// 非阻塞,直接执行
}Go 1.18 之前在运行时用 selectnbsend / selectnbrecv 函数,非阻塞版本的 channel 操作会尝试加锁并尝试发送/接收,如果失败(需要阻塞)就直接返回 false,不走阻塞队列。
五、close 的底层
close(ch) 的底层:
1. 加锁
2. 设置 closed = 1
3. 遍历 recvq,唤醒所有等待的 goroutine(它们会读到零值)
4. 遍历 sendq,唤醒所有等待的 goroutine(它们会收到 panic)
5. 解锁所以向已关闭的 channel 发送数据会 panic,因为 sendq 里的 goroutine 被唤醒后会检查 closed 标志,然后 panic。
六、channel 的 goroutine 数量
// 创建一个 nil channel
var ch chan int
// 发送:永久阻塞
// 接收:永久阻塞
// close:panic
// 创建一个 channel
ch := make(chan int, 1)
// 发送:0 或 1 个 goroutine 在 sendq
// 接收:0 或 1 个 goroutine 在 recvq多个 goroutine 可以同时等待同一个 channel。sendq 和 recvq 是双向链表,每个节点是一个 goroutine 的等待信息。
七、常见坑对照
| 坑 | 现象 | 对策 |
|---|---|---|
| nil channel 发送/接收 | 永久阻塞 | 确保 channel 初始化 |
| 向已关闭 channel 发送 | panic | 只有发送方 close |
| 关闭已关闭的 channel | panic | 用 sync.Once 确保只 close 一次 |
| 不检查是否关闭就 range | range 永远不退出 | 确保发送方 close |
相关与延伸
下一篇:GMP 调度深入 —— 抢占调度、工作窃取;channel 基础用法,见 channel 基础 和 goroutine 与 channel 深度。
一句话总结
channel 底层:hchan 结构包含环形缓冲区 buf、发送/接收 goroutine 队列 sendq/recvq、以及互斥锁;有缓冲 channel 优先直接传递数据(绕过缓冲区),无缓冲 channel 必须同步握手;非阻塞操作(select default)不进入阻塞队列;close 唤醒所有 recvq 和 sendq,sendq 的 goroutine 会 panic;sendq 和 recvq 是双向链表,最多等待 GOMAXPROCS 个 goroutine。