--- 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 传递
绕过缓冲区"] RecvWait -->|否| SpaceAvail{"buf 有空位?"} SpaceAvail -->|是| BufferCopy["拷贝到 buf
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 取出
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 相关高频面试题