2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
2026-06-07 12:14:39 +08:00
|
|
|
|
tags: [go, golang, go-principle, channel]
|
|
|
|
|
|
create time: 2026-06-07 15:25
|
2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
# Channel 底层原理
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 概述
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
本文从 runtime 源码角度深入解析 Go Channel 的数据结构、send/recv 全流程,以及无缓冲和有缓冲 channel 的行为差异。Channel 是 Go CSP 并发模型的基石,理解其底层实现能帮你正确设计并发程序、避免死锁。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!question] ❓ 思考
|
|
|
|
|
|
> 为什么向关闭的 channel 发送数据会 panic,但从关闭的 channel 读取数据不会?当 sendq 和 recvq 同时有等待者时,数据是直接 goroutine 间传递还是经过缓冲区?
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 正文
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 一、Channel 的数据结构:hchan
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
Channel 在 runtime 中用 `hchan` 结构体表示:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// src/runtime/chan.go
|
2026-06-07 11:08:10 +08:00
|
|
|
|
type hchan struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
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 // 保护所有字段的互斥锁
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
用一个有缓冲 channel(buf=8, 已有4个元素)来描述:
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart LR
|
|
|
|
|
|
H["hchan"] --> Q["qcount=4"]
|
|
|
|
|
|
H --> D["dataqsiz=8"]
|
|
|
|
|
|
H --> B["buf: [100][200][300][400][ ][ ][ ][ ]"]
|
|
|
|
|
|
H --> S["sendx=4"]
|
|
|
|
|
|
H --> R["recvx=0"]
|
|
|
|
|
|
H --> SQ["sendq: G1→G2→nil"]
|
|
|
|
|
|
H --> RQ["recvq: nil"]
|
|
|
|
|
|
style B fill:#fff9c4
|
|
|
|
|
|
style SQ fill:#ffebee
|
|
|
|
|
|
style RQ fill:#e8f5e9
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
#### Waitq 与 Sudog
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
type waitq struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
first *sudog
|
|
|
|
|
|
last *sudog
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type sudog struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
g *g // 绑定的 goroutine
|
|
|
|
|
|
elem unsafe.Pointer // 传递的数据
|
|
|
|
|
|
c *hchan // 所属 channel
|
|
|
|
|
|
next *sudog
|
|
|
|
|
|
prev *sudog
|
|
|
|
|
|
success bool // 操作是否成功
|
|
|
|
|
|
// ...
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
`waitq` 是一个双向链表,存储的是 `sudog` 而非 `goroutine`——因为 `sudog` 额外携带了数据地址和成功标志。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 二、Channel 初始化:makechan
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func makechan(t *chantype, size int) *hchan {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
mem := elem.size * uintptr(size)
|
|
|
|
|
|
var c *hchan
|
|
|
|
|
|
switch {
|
|
|
|
|
|
case mem == 0:
|
|
|
|
|
|
// 无缓冲 channel:只分配 hchan
|
|
|
|
|
|
c = (*hchan)(mallocgc(hchanSize, nil, true))
|
|
|
|
|
|
case elem.ptrdata == 0:
|
|
|
|
|
|
// 无指针元素:一次分配 hchan + buf
|
|
|
|
|
|
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
|
|
|
|
|
|
c.buf = add(unsafe.Pointer(c), hchanSize)
|
|
|
|
|
|
default:
|
|
|
|
|
|
// 有指针元素:分开分配
|
|
|
|
|
|
c = new(hchan)
|
|
|
|
|
|
c.buf = mallocgc(mem, elem, true)
|
|
|
|
|
|
}
|
|
|
|
|
|
c.dataqsiz = uint(size)
|
|
|
|
|
|
lockInit(&c.lock, lockRankHchan)
|
|
|
|
|
|
return c
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!note] 📝 源码要点
|
|
|
|
|
|
> 对于不含指针的小元素类型(如 `int`),Go 会把 hchan 和 buf 分配到同一片连续内存中,减少一次 malloc 开销。含指针的类型必须分开分配,否则 GC 无法正确扫描 buf 区域。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 三、Send 操作流程
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
ch <- value // 编译期转换为 chansend(ch, &value, true, getcallerpc())
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
|
|
|
|
|
Start["chansend 开始"] --> NilCh{"ch == nil?"}
|
|
|
|
|
|
NilCh -->|是| ParkNil["gopark 永久挂起"]
|
|
|
|
|
|
NilCh -->|否| Closed{"channel 已关闭?"}
|
|
|
|
|
|
Closed -->|是| Panic["panic: send on closed channel"]
|
|
|
|
|
|
Closed -->|否| Lock["加锁"]
|
|
|
|
|
|
Lock --> RecvWait{"recvq 有等待者?"}
|
|
|
|
|
|
RecvWait -->|是| DirectTransfer["直接 G→G 传递<br/>绕过缓冲区"]
|
|
|
|
|
|
RecvWait -->|否| SpaceAvail{"buf 有空位?"}
|
|
|
|
|
|
SpaceAvail -->|是| BufferCopy["拷贝到 buf<br/>sendx++, qcount++"]
|
|
|
|
|
|
SpaceAvail -->|否| NoSpace{非阻塞?"}
|
|
|
|
|
|
NoSpace -->|是| ReturnFalse["返回 false"]
|
|
|
|
|
|
NoSpace -->|否| CreateSudog["创建 sudog, 加入 sendq"]
|
|
|
|
|
|
CreateSudog --> ParkChan["gopark 挂起"]
|
|
|
|
|
|
DirectTransfer --> Unlock["解锁"]
|
|
|
|
|
|
BufferCopy --> Unlock
|
|
|
|
|
|
ParkChan --> Woken["被 recv 唤醒后解锁"]
|
|
|
|
|
|
Unlock --> Done["返回 true"]
|
|
|
|
|
|
Woken --> Done
|
|
|
|
|
|
style DirectTransfer fill:#e8f5e9
|
|
|
|
|
|
style BufferCopy fill:#fff9c4
|
|
|
|
|
|
style Panic fill:#ffebee
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
关键场景分析:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
| 场景 | 行为 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| ch == nil | 永远阻塞 |
|
|
|
|
|
|
| 已关闭 | panic |
|
|
|
|
|
|
| recvq 有等待者 | **G→G 直接传递**,不经过 buf |
|
|
|
|
|
|
| buf 有空位 | 拷贝到 buf,立即返回 |
|
|
|
|
|
|
| buf 满且 recvq 空 | sudog 入队,gopark 挂起 |
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!tip] 💡 理解要点
|
|
|
|
|
|
> 无缓冲 channel 的 send/recv 本质上是"握手"过程——发送方和接收方同时准备好时,数据直接从发送方的栈拷贝到接收方的栈,完全不经过 hchan 内部。
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 四、Recv 操作流程
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
value := <-ch // 编译期转换为 chanrecv(ch, &value, true)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
|
|
|
|
|
Start["chanrecv 开始"] --> NilCh{"ch == nil?"}
|
|
|
|
|
|
NilCh -->|是| ParkNil["gopark 永久挂起"]
|
|
|
|
|
|
NilCh -->|否| Closed{"closed 且 buf 空?"}
|
|
|
|
|
|
Closed -->|是| ZeroValue["返回零值, ok=false"]
|
|
|
|
|
|
Closed -->|否| Lock["加锁"]
|
|
|
|
|
|
Lock --> SendWait{"sendq 有等待者?"}
|
|
|
|
|
|
SendWait -->|是| DirectRecv["G→G 直接接收"]
|
|
|
|
|
|
SendWait -->|否| BufHasData{"buf 有数据?"}
|
|
|
|
|
|
BufHasData -->|是| BufCopy["从 buf 取出<br/>recvx++, qcount--"]
|
|
|
|
|
|
BufHasData -->|否| NoData{非阻塞?"}
|
|
|
|
|
|
NoData -->|是| ReturnFF["返回 false, false"]
|
|
|
|
|
|
NoData -->|否| CreateSudog["创建 sudog, 加入 recvq"]
|
|
|
|
|
|
CreateSudog --> ParkChan["gopark 挂起"]
|
|
|
|
|
|
DirectRecv --> Unlock["解锁"]
|
|
|
|
|
|
BufCopy --> Unlock
|
|
|
|
|
|
ParkChan --> Woken["被 send 唤醒后解锁"]
|
|
|
|
|
|
Unlock --> Done{"ok?"}
|
|
|
|
|
|
Woken --> Done
|
|
|
|
|
|
style DirectRecv fill:#e8f5e9
|
|
|
|
|
|
style BufCopy fill:#fff9c4
|
|
|
|
|
|
style ZeroValue fill:#fff3e0
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 五、Close 流程
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
2026-06-07 12:14:39 +08:00
|
|
|
|
close(ch) // 编译期转换为 closechan(ch)
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
func closechan(c *hchan) {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
if c == nil { panic("close of nil channel") }
|
|
|
|
|
|
lock(&c.lock)
|
|
|
|
|
|
if c.closed != 0 { panic("close of closed channel") }
|
|
|
|
|
|
c.closed = 1
|
|
|
|
|
|
|
|
|
|
|
|
var glist gList
|
|
|
|
|
|
// 1. 唤醒所有 recvq 中的 goroutine(返回零值)
|
|
|
|
|
|
for { sg := c.recvq.dequeue(); if sg == nil { break }
|
|
|
|
|
|
sg.elem = nil; sg.success = false; glist.push(sg.g)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 2. 唤醒所有 sendq 中的 goroutine(触发 panic)
|
|
|
|
|
|
for { sg := c.sendq.dequeue(); if sg == nil { break }
|
|
|
|
|
|
sg.elem = nil; sg.success = false; glist.push(sg.g)
|
|
|
|
|
|
}
|
|
|
|
|
|
unlock(&c.lock)
|
|
|
|
|
|
// 3. 批量恢复调度
|
|
|
|
|
|
for !glist.empty() { goready(glist.pop(), 3) }
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
> [!warning] ⚠️ 注意
|
|
|
|
|
|
> - 关闭已关闭的 channel → panic
|
|
|
|
|
|
> - 向已关闭的 channel 发送数据 → panic
|
|
|
|
|
|
> - 从已关闭的 channel 读取数据 → 返回零值和 `false`(直到 buf 排空)
|
|
|
|
|
|
> - 确保发送方全部完成后才关闭 channel,否则 panic
|
|
|
|
|
|
|
|
|
|
|
|
### 六、性能建议
|
|
|
|
|
|
|
|
|
|
|
|
1. **预知容量时指定 buffer size**:避免运行时动态分配
|
|
|
|
|
|
2. **优先使用有缓冲 channel**:减少 goroutine 阻塞概率
|
|
|
|
|
|
3. **关闭 channel 的责任归属**:通常由发送方负责关闭,接收方不应关闭
|
|
|
|
|
|
4. **无缓冲 channel 适合同步信号**:`make(chan struct{})` 是最轻量的同步方式
|
|
|
|
|
|
|
|
|
|
|
|
## 小结
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
- Channel 核心是 `(buf, sendx, recvx)` 环形队列 + `sendq/recvq` 等待链表
|
|
|
|
|
|
- 无缓冲 channel 的 send/recv 是 G→G 直接传递,不经过 buf
|
|
|
|
|
|
- 关闭 channel 会批量唤醒所有等待者
|
|
|
|
|
|
- 理解 channel 底层能有效避免死锁和误用
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 关联笔记
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Channel]] — Channel 的基础用法
|
|
|
|
|
|
- [[hzh/GolangStar/Go语言进阶/Select]] — Select 多路复用
|
|
|
|
|
|
- [[hzh/GolangStar/Go面试题库/Channel面试题]] — Channel 相关高频面试题
|