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

231 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [go, golang, go-principle, channel]
create time: 2026-06-07 15:25
---
# Channel 底层原理
## 概述
本文从 runtime 源码角度深入解析 Go Channel 的数据结构、send/recv 全流程,以及无缓冲和有缓冲 channel 的行为差异。Channel 是 Go CSP 并发模型的基石,理解其底层实现能帮你正确设计并发程序、避免死锁。
> [!question] ❓ 思考
> 为什么向关闭的 channel 发送数据会 panic,但从关闭的 channel 读取数据不会?当 sendq 和 recvq 同时有等待者时,数据是直接 goroutine 间传递还是经过缓冲区?
## 正文
### 一、Channel 的数据结构:hchan
Channel 在 runtime 中用 `hchan` 结构体表示:
```go
// src/runtime/chan.go
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 // 保护所有字段的互斥锁
}
```
用一个有缓冲 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
```go
type waitq struct {
first *sudog
last *sudog
}
type sudog struct {
g *g // 绑定的 goroutine
elem unsafe.Pointer // 传递的数据
c *hchan // 所属 channel
next *sudog
prev *sudog
success bool // 操作是否成功
// ...
}
```
`waitq` 是一个双向链表,存储的是 `sudog` 而非 `goroutine`——因为 `sudog` 额外携带了数据地址和成功标志。
### 二、Channel 初始化:makechan
```go
func makechan(t *chantype, size int) *hchan {
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
}
```
> [!note] 📝 源码要点
> 对于不含指针的小元素类型(如 `int`),Go 会把 hchan 和 buf 分配到同一片连续内存中,减少一次 malloc 开销。含指针的类型必须分开分配,否则 GC 无法正确扫描 buf 区域。
### 三、Send 操作流程
```go
ch <- value // 编译期转换为 chansend(ch, &value, true, getcallerpc())
```
```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
```
关键场景分析:
| 场景 | 行为 |
|------|------|
| ch == nil | 永远阻塞 |
| 已关闭 | panic |
| recvq 有等待者 | **G→G 直接传递**,不经过 buf |
| buf 有空位 | 拷贝到 buf,立即返回 |
| buf 满且 recvq 空 | sudog 入队,gopark 挂起 |
> [!tip] 💡 理解要点
> 无缓冲 channel 的 send/recv 本质上是"握手"过程——发送方和接收方同时准备好时,数据直接从发送方的栈拷贝到接收方的栈,完全不经过 hchan 内部。
### 四、Recv 操作流程
```go
value := <-ch // 编译期转换为 chanrecv(ch, &value, true)
```
```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
```
### 五、Close 流程
```go
close(ch) // 编译期转换为 closechan(ch)
```
```go
func closechan(c *hchan) {
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) }
}
```
> [!warning] ⚠️ 注意
> - 关闭已关闭的 channel → panic
> - 向已关闭的 channel 发送数据 → panic
> - 从已关闭的 channel 读取数据 → 返回零值和 `false`(直到 buf 排空)
> - 确保发送方全部完成后才关闭 channel,否则 panic
### 六、性能建议
1. **预知容量时指定 buffer size**:避免运行时动态分配
2. **优先使用有缓冲 channel**:减少 goroutine 阻塞概率
3. **关闭 channel 的责任归属**:通常由发送方负责关闭,接收方不应关闭
4. **无缓冲 channel 适合同步信号**:`make(chan struct{})` 是最轻量的同步方式
## 小结
- Channel 核心是 `(buf, sendx, recvx)` 环形队列 + `sendq/recvq` 等待链表
- 无缓冲 channel 的 send/recv 是 G→G 直接传递,不经过 buf
- 关闭 channel 会批量唤醒所有等待者
- 理解 channel 底层能有效避免死锁和误用
## 关联笔记
- [[hzh/GolangStar/Go语言进阶/Channel]] — Channel 的基础用法
- [[hzh/GolangStar/Go语言进阶/Select]] — Select 多路复用
- [[hzh/GolangStar/Go面试题库/Channel面试题]] — Channel 相关高频面试题