Files
cs-note/hzh/GolangStar/Go语言原理/channel原理.md
T

231 lines
7.6 KiB
Markdown
Raw Normal View History

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 相关高频面试题