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

7.6 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go-principle
channel
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 结构体表示:

// 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个元素)来描述:

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

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

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 操作流程

ch <- value  // 编译期转换为 chansend(ch, &value, true, getcallerpc())
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 操作流程

value := <-ch  // 编译期转换为 chanrecv(ch, &value, true)
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 流程

close(ch)  // 编译期转换为 closechan(ch)
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 底层能有效避免死锁和误用

关联笔记