Golang Channel 深度解析

基于 Go 1.26.4 源码,源码路径:github.com/go-go1.26.4
核心源文件:runtime/chan.goruntime/select.go


1 channel 功能完整介绍

1.1 语法定义与核心概念

Go 通道(channel)是 goroutine 间通信的核心原语,遵循 CSP(Communicating Sequential Processes) 模型:

“Don’t communicate by sharing memory; share memory by communicating.”

// 声明(零值 nil)
var ch chan int               // 双向通道,nil
var chSend chan<- int         // 只写通道
var chRecv <-chan int         // 只读通道

// make 初始化
ch := make(chan int)          // 无缓冲通道(同步通道)
ch := make(chan int, 10)     // 有缓冲通道(异步通道),容量=10

// 操作
ch <- value                  // 发送(阻塞直到有人接收)
v := <-ch                    // 接收(阻塞直到有人发送)
v, ok := <-ch                // 接收 + 判断通道是否已关闭
close(ch)                    // 关闭通道
cap(ch)                      // 容量
len(ch)                      // 缓冲区中元素数量

在这里插入图片描述

1.2 三种通道类型对比

类型 定义 发送行为 接收行为 典型用途
无缓冲通道 make(chan T) 阻塞直到有接收者 阻塞直到有发送者 同步信号、握手
有缓冲通道 make(chan T, n) 缓冲区满时阻塞 缓冲区空时阻塞 生产者-消费者、限流
nil 通道 var ch chan T 永久阻塞 永久阻塞 select 中禁用分支

1.3 适用场景

Go Channel 适用场景

goroutine 通信

信号通知 done chan

数据传递 pipeline

并发控制

限流 buffered chan

等待组替代

生产者消费者

任务队列

结果收集

超时控制

select + time.After

context.Cancel

退出信号

close 通知所有接收者

done channel 模式


2 底层 runtime 实现原理

2.1 hchan 结构体完整字段说明

源码位置:runtime/chan.go:20
在这里插入图片描述

type hchan struct {
    qcount   uint           // ★ 缓冲区中已有元素数量
                           //   len(ch) 直接读取此字段
                           //   仅对有缓冲通道有意义

    dataqsiz uint           // ★ 缓冲区容量(环形队列大小)
                           //   cap(ch) 直接读取此字段
                           //   无缓冲通道 = 0

    buf      unsafe.Pointer // ★ 指向环形队列缓冲区
                           //   dataqsiz * elemsize 大小的连续内存
                           //   无缓冲通道指向 hchan 自身的一个地址(raceaddr)
                           //   有缓冲通道指向独立分配的数组

    elemsize uint16         // 单个元素大小(字节)
    closed   uint32         // ★ 关闭标志:0=未关闭, 1=已关闭
    timer    *timer         // 关联的定时器(time.Tick/time.After 用)

    elemtype *_type         // ★ 元素类型元数据
                           //   用于 typedmemmove/typedmemclr
                           //   包含大小、对齐、GC 信息

    sendx    uint           // ★ 发送索引:下一个写入位置
                           //   范围 [0, dataqsiz)
                           //   写入后 sendx = (sendx+1) % dataqsiz

    recvx    uint           // ★ 接收索引:下一个读取位置
                           //   范围 [0, dataqsiz)
                           //   读取后 recvx = (recvx+1) % dataqsiz

    recvq    waitq          // ★ 等待接收的 goroutine 队列
                           //   缓冲区空时,接收者在此排队

    sendq    waitq          // ★ 等待发送的 goroutine 队列
                           //   缓冲区满时,发送者在此排队

    bubble   *synctestBubble // synctest 气泡(测试用)

    lock     mutex          // ★ 互斥锁
                           //   保护 hchan 所有字段
                           //   保护阻塞在此通道上的 sudog 字段
}

waitq 等待队列

type waitq struct {
    first *sudog            // 队首
    last  *sudog            // 队尾
}

sudog(sudog = pseudo-G)

在这里插入图片描述


type sudog struct {
    g     *g               // 关联的 goroutine
    elem  unsafe.Pointer   // 指向数据的指针(发送/接收的元素地址)
    next  *sudog           // 链表下一个
    prev  *sudog           // 链表上一个
    c     *hchan            // 关联的 channel
    success bool           // 操作是否成功
    isSelect  bool         // 是否来自 select
    waitlink  *sudog       // g.waiting 链表
    releasetime int64       // 阻塞分析时间戳
}

hchan 完整内存布局

hchan 结构体 (96 bytes on 64-bit)
┌─────────────────────────────────────────────────────────┐
│  qcount    uint      (8B) │ 缓冲区元素数              │
├─────────────────────────────────────────────────────────┤
│  dataqsiz  uint      (8B) │ 缓冲区容量                │
├─────────────────────────────────────────────────────────┤
│  buf       unsafe.P  (8B) │ 环形队列指针 ──→ [slot0][slot1]...[slotN-1]│
├─────────────────────────────────────────────────────────┤
│  elemsize  uint16    (2B) │ 元素大小                  │
│  closed    uint32    (4B) │ 关闭标志                  │
├─────────────────────────────────────────────────────────┤
│  timer     *timer    (8B) │ 定时器                    │
├─────────────────────────────────────────────────────────┤
│  elemtype  *_type    (8B) │ 元素类型元数据            │
├─────────────────────────────────────────────────────────┤
│  sendx     uint      (8B) │ 发送索引                  │
├─────────────────────────────────────────────────────────┤
│  recvx     uint      (8B) │ 接收索引                  │
├─────────────────────────────────────────────────────────┤
│  recvq     waitq    (16B) │ 等待接收队列 ──→ sudog链表│
│           {first,last}    │                           │
├─────────────────────────────────────────────────────────┤
│  sendq     waitq    (16B) │ 等待发送队列 ──→ sudog链表│
│           {first,last}    │                           │
├─────────────────────────────────────────────────────────┤
│  bubble    *synctestB(8B) │ 测试气泡                  │
├─────────────────────────────────────────────────────────┤
│  lock      mutex     (8B) │ 互斥锁                    │
└─────────────────────────────────────────────────────────┘

2.2 环形队列缓冲区

有缓冲 channel 在底层使用 环形队列 (ring buffer) 存储数据。sendx/recvx 分别标记写入和读取位置。

    dataqsiz = 6 (容量为 6 的环形缓冲区)

                      sendx (写入位置)
                          │
                          ▼
    ┌─────┬─────┬─────┬─────┬─────┬─────┐
    │  42 │  17 │  35 │     │     │     │
    └──▲──┴─────┴─────┴─────┴─────┴─────┘
       │
       └──── recvx (读取位置)

    当前状态: qcount = 3, 元素: [42, 17, 35]

    ── 写入一个元素 99 后 ──

                      sendx (向前移动, 取模 dataqsiz)
                          │
                          ▼
    ┌─────┬─────┬─────┬─────┬─────┬─────┐
    │  42 │  17 │  35 │  99 │     │     │
    └──▲──┴─────┴─────┴─────┴─────┴─────┘
       │
       └──── recvx (不变, 满时 = sendx)

    ── 读取一个元素后 ──

                    sendx
                      │
                      ▼
    ┌─────┬─────┬─────┬─────┬─────┬─────┐
    │     │  17 │  35 │  99 │     │     │
    └─────┴──▲──┴─────┴─────┴─────┴─────┘
             │
             └──── recvx (向前移动)

    关键公式:
    • 元素地址 = buf + recvx * elemsize   (读取)
    • 元素地址 = buf + sendx * elemsize   (写入)
    • recvx == sendx 且 qcount == 0  → 空
    • recvx == sendx 且 qcount > 0   → 满

状态判断

环形队列操作

qcount == dataqsiz

qcount == 0

发送: buf[sendx] = value
sendx = (sendx+1) % cap
qcount++

接收: value = buf[recvx]
recvx = (recvx+1) % cap
qcount--

满: qcount == dataqsiz
发送阻塞 → sendq

空: qcount == 0
接收阻塞 → recvq

环形缓冲区的优势:避免频繁的内存分配和释放。数据直接在预分配的 buf 上读写,通过索引取模实现循环。当 dataqsiz 为 2 的幂时,取模优化为位运算 & (dataqsiz - 1)。

2.3 发送操作完整流程(chansend)

源码位置:runtime/chan.go:159

func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
    // ════════════════════════════════════════════
    // Step 0: nil channel 检查
    // ════════════════════════════════════════════
    if c == nil {
        if !block { return false }  // 非阻塞(select default)→ 返回 false
        gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
        // ★ 向 nil 通道发送 = 永久阻塞!永远不会被唤醒
        throw("unreachable")
    }

    // ════════════════════════════════════════════
    // Step 1: 快速路径——非阻塞发送失败检测(不加锁)
    // ════════════════════════════════════════════
    if !block && c.closed == 0 && full(c) {
        return false
        // full() 检查:
        //   无缓冲: recvq.first == nil(没有等待的接收者)
        //   有缓冲: qcount == dataqsiz(缓冲区满)
    }

    lock(&c.lock)

    // ════════════════════════════════════════════
    // Step 2: 检查通道是否已关闭
    // ════════════════════════════════════════════
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("send on closed channel"))
        // ★ 向已关闭通道发送 → panic!
    }

    // ════════════════════════════════════════════
    // Step 3: 有等待的接收者?直接发送!
    // ════════════════════════════════════════════
    if sg := c.recvq.dequeue(); sg != nil {
        send(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true
        // ★ 绕过缓冲区,直接从发送者拷贝到接收者栈!
        // 这是无缓冲通道和有缓冲通道共用的快速路径
    }

    // ════════════════════════════════════════════
    // Step 4: 缓冲区有空间?写入缓冲区
    // ════════════════════════════════════════════
    if c.qcount < c.dataqsiz {
        qp := chanbuf(c, c.sendx)           // buf + sendx * elemsize
        typedmemmove(c.elemtype, qp, ep)    // 拷贝数据到缓冲区
        c.sendx++                            // 移动发送索引
        if c.sendx == c.dataqsiz {
            c.sendx = 0                      // 环形回绕
        }
        c.qcount++                           // 元素计数+1
        unlock(&c.lock)
        return true
    }

    // ════════════════════════════════════════════
    // Step 5: 非阻塞模式 + 缓冲区满 → 返回失败
    // ════════════════════════════════════════════
    if !block {
        unlock(&c.lock)
        return false
    }

    // ════════════════════════════════════════════
    // Step 6: 阻塞当前 goroutine
    // ════════════════════════════════════════════
    gp := getg()
    mysg := acquireSudog()            // 从 sudog 池获取
    mysg.elem.set(ep)                 // 记录发送数据的地址
    mysg.g = gp
    mysg.c.set(c)
    gp.waiting = mysg
    gp.param = nil
    c.sendq.enqueue(mysg)             // ★ 加入发送等待队列
    gp.parkingOnChan.Store(true)
    gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
    // ★ 当前 G 被挂起(parking),让出 CPU
    //   等待接收者来取数据后唤醒

    // ════════════════════════════════════════════
    // Step 7: 被唤醒后的清理
    // ════════════════════════════════════════════
    gp.waiting = nil
    gp.activeStackChans = false
    closed := !mysg.success
    mysg.c.set(nil)
    releaseSudog(mysg)                // 归还 sudog 到池
    if closed {
        panic(plainError("send on closed channel"))
    }
    return true
}

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

ch <- value
chansend()

c == nil?

永久阻塞 gopark
(永不唤醒)

快速路径:
非阻塞 && 未关闭 && 满?

return false

lock(&c.lock)

c.closed != 0?

panic!
send on closed channel

recvq 有等待者?

★ 直接发送
send(c, sg, ep)
绕过缓冲区
唤醒接收者

qcount < dataqsiz?

写入缓冲区
buf[sendx] = value
sendx++
qcount++

非阻塞?

return false

★ 阻塞
加入 sendq
gopark 挂起

被接收者唤醒
releaseSudog
return true

三种路径:
    ┌─────────────────┬──────────────────┬───────────────────┐
    │ 直接发送         │ 写入缓冲区         │ 阻塞等待           │
    │ (recvq 非空)(缓冲区有空位)(缓冲区满)         │
    ├─────────────────┼──────────────────┼───────────────────┤
    │ 数据直接拷贝到    │ 数据拷贝到         │ goroutine 挂起     │
    │ 接收者的栈       │ 环形缓冲区          │ 等待接收者取走      │
    └─────────────────┴──────────────────┴───────────────────┘
2.3.1 直接发送 — 有接收者等待

当发送时发现 recvq 非空(即有 goroutine 正阻塞等待接收),channel 走"直接发送"路径。

    场景: ch <- 42, 同时有 G2 在等待 <-ch

    发送前:

      hchan
      ┌─────────────────────────────┐
      │ recvq        │  sudog链表    │
      │              │ ┌──────┐     │          ┌──────────┐
      │              │ │ G2   │     │   elem──►│ G2 栈帧  │
      │              │ │ elem─┼─────┼─────────►│ ()     │
      │              │ └──────┘     │          └──────────┘
      │ dataqsiz=0(无缓冲)     │
      └─────────────────────────────┘

    发送: ch <- 42

      ┌──────────┐
      │ G1 栈帧  │     42  (值拷贝到 G2.elem 指向的地址)42       │─────►
      └──────────┘       │
                         ▼
                    ┌──────────┐
                    │ G2 栈帧  │
                    │ 42 ← 写入  │
                    └──────────┘

    然后: goready(G2)  — 将 G2 放回运行队列

sendDirect 将发送方的数据直接 memmove 到接收方 sudog.elem 所指向的地址(即接收方 goroutine 的栈上变量地址)。这个操作是 untyped memcpy,不经过中间缓冲区,因此性能很高。

在这里插入图片描述

2.4 接收操作完整流程(chanrecv)

源码位置:runtime/chan.go:462

func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
    // Step 0: nil channel → 永久阻塞
    if c == nil {
        if !block { return }
        gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)
        throw("unreachable")
    }

    // Step 1: 快速路径——非阻塞接收失败检测
    if !block && empty(c) {
        if atomic.Load(&c.closed) == 0 { return }
        // 通道已关闭且空 → 返回零值
        if empty(c) { return true, false }
    }

    lock(&c.lock)

    // Step 2: 通道已关闭 + 缓冲区空 → 返回零值
    if c.closed != 0 && c.qcount == 0 {
        unlock(&c.lock)
        if ep != nil { typedmemclr(c.elemtype, ep) }  // 清零
        return true, false   // selected=true, received=false
    }

    // Step 3: 有等待的发送者?直接接收!
    if sg := c.sendq.dequeue(); sg != nil {
        recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
        return true, true
        // ★ 关键区别:
        //   无缓冲通道:直接从发送者拷贝到接收者
        //   有缓冲通道:从缓冲区头部读取,发送者数据放入缓冲区尾部
    }

    // Step 4: 缓冲区有数据?直接读取
    if c.qcount > 0 {
        qp := chanbuf(c, c.recvx)
        if ep != nil { typedmemmove(c.elemtype, ep, qp) }
        typedmemclr(c.elemtype, qp)    // 清空缓冲区 slot
        c.recvx++
        if c.recvx == c.dataqsiz { c.recvx = 0 }
        c.qcount--
        unlock(&c.lock)
        return true, true
    }

    // Step 5: 非阻塞 + 缓冲区空 → 返回失败
    if !block {
        unlock(&c.lock)
        return false, false
    }

    // Step 6: 阻塞当前 goroutine
    gp := getg()
    mysg := acquireSudog()
    mysg.elem.set(ep)
    mysg.g = gp
    mysg.c.set(c)
    c.recvq.enqueue(mysg)             // ★ 加入接收等待队列
    gopark(chanparkcommit, ...)

    // Step 7: 被唤醒后清理
    success := mysg.success
    releaseSudog(mysg)
    return true, success
}

recv 函数的关键逻辑——有缓冲通道满时的接收:

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if c.dataqsiz == 0 {
        // 无缓冲:直接从发送者 → 接收者
        if ep != nil { recvDirect(c.elemtype, sg, ep) }
    } else {
        // 有缓冲(且满):
        //   1. 从缓冲区头部取出数据给接收者
        //   2. 发送者的数据放入缓冲区尾部
        //   → 因为缓冲区满,头和尾是同一个 slot!
        qp := chanbuf(c, c.recvx)
        if ep != nil { typedmemmove(c.elemtype, ep, qp) }    // 1. 取出头部
        typedmemmove(c.elemtype, qp, sg.elem.get())          // 2. 放入尾部
        c.recvx++
        if c.recvx == c.dataqsiz { c.recvx = 0 }
        c.sendx = c.recvx   // ★ 因为满,sendx 追上 recvx
    }
    sg.elem.set(nil)
    gp := sg.g
    unlockf()
    goready(gp, skip+1)   // ★ 唤醒发送者
}

Yes

No

Yes

No

Yes

No

Yes

No

Yes

No

<-ch
chanrecv()

c == nil?

永久阻塞

lock

closed && qcount==0?

返回零值
(true, false)

sendq 有等待者?

★ 直接接收
无缓冲: 发送者→接收者
有缓冲: 头部出+尾部入

qcount > 0?

读取缓冲区
buf[recvx]
recvx++
qcount--

非阻塞?

return false

★ 阻塞
加入 recvq
gopark 挂起

在这里插入图片描述

2.5 close 操作完整流程(closechan)

源码位置:runtime/chan.go:383

func closechan(c *hchan) {
    // nil channel → panic
    if c == nil { panic(plainError("close of nil channel")) }

    lock(&c.lock)
    // 已关闭 → panic
    if c.closed != 0 {
        unlock(&c.lock)
        panic(plainError("close of closed channel"))
    }

    c.closed = 1  // ★ 标记为已关闭

    var glist gList

    // ★ 释放所有等待的接收者
    for {
        sg := c.recvq.dequeue()
        if sg == nil { break }
        if sg.elem.get() != nil {
            typedmemclr(c.elemtype, sg.elem.get())  // 清零接收者的 elem
        }
        gp := sg.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false   // ★ 标记:因为通道关闭而唤醒
        glist.push(gp)
    }

    // ★ 释放所有等待的发送者(他们会 panic)
    for {
        sg := c.sendq.dequeue()
        if sg == nil { break }
        sg.elem.set(nil)
        gp := sg.g
        gp.param = unsafe.Pointer(sg)
        sg.success = false
        glist.push(gp)       // 发送者唤醒后会 panic("send on closed channel")
    }
    unlock(&c.lock)

    // ★ 批量唤醒所有 goroutine(在锁外执行,避免死锁)
    for !glist.empty() {
        gp := glist.pop()
        goready(gp, 3)       // 将 G 加入运行队列
    }
}

在这里插入图片描述

close 语义总结

close(ch) 后

仍然可以接收
直到缓冲区空

全部唤醒
返回零值

全部唤醒
然后 panic

立即 panic

缓冲区空后
返回零值

panic

缓冲区数据

接收返回 (value, false)

等待的接收者

received=false

等待的发送者

💥 send on closed

新发送

💥 send on closed

新接收

received=false

重复 close

💥 close of closed

2.6 makechan 内存分配策略

源码位置:runtime/chan.go:52

func makechan(t *chantype, size int) *hchan {
    elem := t.Elem
    mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))

    var c *hchan
    switch {
    case mem == 0:
        // ★ 无缓冲通道(size=0)或元素大小为0
        // 只分配 hchan 结构体,不分配缓冲区
        c = (*hchan)(mallocgc(hchanSize, nil, true))
        c.buf = c.raceaddr()  // buf 指向 hchan 内部一个地址(仅 race 用)

    case !elem.Pointers():
        // ★ 元素不含指针(如 int, float64)
        // 一次性分配 hchan + 缓冲区(连续内存,减少一次分配)
        c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
        c.buf = add(unsafe.Pointer(c), hchanSize)  // buf 紧跟 hchan 后面

    default:
        // ★ 元素含指针(如 *int, string, struct含指针)
        // 分开分配:hchan 和缓冲区各自独立
        // 因为 buf 中有指针,需要 GC 扫描
        c = new(hchan)
        c.buf = mallocgc(mem, elem, true)
    }

    c.elemsize = uint16(elem.Size_)
    c.elemtype = elem
    c.dataqsiz = uint(size)
    lockInit(&c.lock, lockRankHchan)
    return c
}
三种内存分配方式:

1. 无缓冲通道 (size=0):
   ┌──────────────┐
   │   hchan      │  mallocgc(hchanSize)
   │   buf ──→ ──┤──→ 指向 hchan 自身内部
   └──────────────┘

2. 有缓冲 + 元素无指针 (如 chan int, cap=4):
   ┌──────────────┬───────┬───────┬───────┬───────┐
   │   hchan      │slot[0]│slot[1]│slot[2]│slot[3]│  一次分配
   │   buf ───────│───────│───────│───────│───────│
   └──────────────┴───────┴───────┴───────┴───────┘
   mallocgc(hchanSize + 4*8)

3. 有缓冲 + 元素有指针 (如 chan *int, cap=4):
   ┌──────────────┐       ┌───────┬───────┬───────┬───────┐
   │   hchan      │       │slot[0]│slot[1]│slot[2]│slot[3]│
   │   buf ─────────────→ │       │       │       │       │
   └──────────────┘       └───────┴───────┴───────┴───────┘
   new(hchan)              mallocgc(mem, elem, true)

2.7 直接发送 send()——跨 goroutine 栈拷贝

这是 Go 中唯一一个 goroutine 写另一个 goroutine 栈的场景:

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    if sg.elem.get() != nil {
        sendDirect(c.elemtype, sg, ep)
        // ★ 直接从发送者的栈 → 接收者的栈 拷贝数据!
        // 普通情况下 goroutine 只能写自己的栈
        // 这是唯一例外,因为此时接收者已 gopark,栈不会变化
    }
    gp := sg.g
    unlockf()
    gp.param = unsafe.Pointer(sg)
    sg.success = true
    goready(gp, skip+1)   // 唤醒接收者
}

func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
    dst := sg.elem.get()  // 接收者栈上的目标地址
    typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.Size_)
    memmove(dst, src, t.Size_)  // 跨栈内存拷贝
}
接收者 G (parked) hchan 发送者 G 接收者 G (parked) hchan 发送者 G memmove(dst=R的栈, src=S的栈, size) lock recvq.dequeue() → 找到接收者 sendDirect: 直接写入接收者栈! unlock goready(R) → 唤醒接收者 从 gopark 返回,数据已在栈上

2.8 hchan 不变量(核心约束)

源码注释中明确了以下不变量:

1. c.sendq 和 c.recvq 至少一个为空
   (例外:无缓冲通道 + select 中同时发送和接收)

2. 有缓冲通道:
   - qcount > 0 → recvq 为空(有数据可读,不需等待)
   - qcount < dataqsiz → sendq 为空(有空间可写,不需等待)

3. 关闭后:
   - 不能再发送(panic)
   - 缓冲区数据仍可接收
   - 缓冲区空后接收返回零值

3 示例代码逐行解析

3.1 无缓冲通道——同步握手

ch := make(chan int)
// [运行时] makechan(t, 0)
//   dataqsiz = 0, buf = raceaddr
//   sendq = {nil, nil}, recvq = {nil, nil}

// ── goroutine A: 发送 ──
go func() {
    ch <- 42
    // [运行时] chansend(c, &42, true, pc):
    //   1. c != nil → 继续
    //   2. full(c) → dataqsiz==0 && recvq.first==nil → true(没有等待接收者)
    //   3. lock(&c.lock)
    //   4. c.closed == 0 → 继续
    //   5. recvq.dequeue() → nil(没有等待接收者)
    //   6. qcount(0) < dataqsiz(0)? → false
    //   7. block=true → 阻塞
    //      acquireSudog → enqueue to sendq → gopark
    //      ★ goroutine A 挂起,等待接收者
    
    // 之后 goroutine B 接收时:
    //   recvq 没有,sendq 有 A
    //   recv(c, sg_A, ep, unlockf, 3):
    //     dataqsiz == 0 → recvDirect: 直接从 A 的栈拷贝到 B 的栈
    //     goready(A) → 唤醒 A
    //   A 从 gopark 返回 → return true
}()

// ── goroutine B: 接收 ──
v := <-ch
// [运行时] chanrecv(c, &v, true):
//   如果 A 先到且已阻塞在 sendq:
//     1. lock
//     2. sendq.dequeue() → 找到 A
//     3. recv(c, sg_A, &v, unlockf):
//        dataqsiz==0 → recvDirect: 直接从 A 的栈 → B 的栈
//     4. goready(A) → 唤醒 A
//     5. return true, true

//   如果 B 先到(A 还没发送):
//     1. lock
//     2. sendq.dequeue() → nil
//     3. qcount(0) == 0 → 缓冲区空
//     4. 阻塞:加入 recvq → gopark
//     5. A 后发送时:chansend 发现 recvq 有 B → send 直接传递

3.2 有缓冲通道——异步收发

ch := make(chan int, 3)
// [运行时] makechan(t, 3)
//   dataqsiz = 3, buf = mallocgc(3*8, int类型, false)
//   因为 int 无指针,一次分配 hchan + buf

ch <- 10  // 发送1
// [运行时] chansend:
//   recvq 为空 → 不走直接发送
//   qcount(0) < dataqsiz(3) → 写入缓冲区
//     buf[sendx=0] = 10, sendx=1, qcount=1
//   return true ✅ 不阻塞

ch <- 20  // 发送2
//   buf[sendx=1] = 20, sendx=2, qcount=2

ch <- 30  // 发送3
//   buf[sendx=2] = 30, sendx=0 (回绕), qcount=3
//   ★ 缓冲区满!

ch <- 40  // 发送4 → 阻塞
// [运行时] chansend:
//   recvq 为空, qcount(3) == dataqsiz(3) → 缓冲区满
//   block=true → 阻塞,加入 sendq

v1 := <-ch  // 接收1
// [运行时] chanrecv:
//   sendq 有等待者 → recv(c, sg, &v1, unlockf):
//     dataqsiz > 0:
//       qp = buf[recvx=0] = 10
//       v1 = 10                    ← 从缓冲区取出
//       buf[0] = sg.elem(40)       ← 发送者数据放入
//       recvx = 1, sendx = 1
//   goready(发送者) → 唤醒
//   ★ 接收者拿到缓冲区头部,发送者数据补到尾部

3.3 close 逐行解析

close(ch)
// [运行时] closechan(c):
//   1. c == nil → panic("close of nil channel")
//   2. lock(&c.lock)
//   3. c.closed != 0 → panic("close of closed channel")
//   4. c.closed = 1
//   5. 释放 recvq 所有等待者:sg.success = false, goready
//   6. 释放 sendq 所有等待者:sg.success = false, goready
//      (发送者唤醒后检查 !mysg.success → panic)
//   7. unlock
//   8. 批量 goready 所有被释放的 G

// close 后的接收
v, ok := <-ch
// [运行时] chanrecv:
//   如果缓冲区还有数据 → 继续读取,ok=true
//   如果缓冲区空 → typedmemclr(零值), return (true, false)
//                                      selected=true, received=false

// close 后的发送
ch <- 1
// [运行时] chansend:
//   c.closed != 0 → panic("send on closed channel") 💥

4 完整使用示例合集

4.1 基础收发完整可运行代码

package main

import "fmt"

func main() {
    // 无缓冲通道——同步通信
    done := make(chan struct{})
    go func() {
        fmt.Println("working...")
        done <- struct{}{} // 发送完成信号
    }()
    <-done // 等待完成
    fmt.Println("done")

    // 有缓冲通道——异步通信
    ch := make(chan string, 2)
    ch <- "hello"     // 不阻塞(缓冲区有空间)
    ch <- "world"     // 不阻塞(缓冲区刚好满)
    fmt.Println(<-ch) // hello
    fmt.Println(<-ch) // world
}

4.2 单向通道与通道方向约束

func producer(ch chan<- int) {  // 只写通道
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(ch <-chan int) {  // 只读通道
    for v := range ch {
        fmt.Println(v)
    }
}

func main() {
    ch := make(chan int, 5)
    go producer(ch)   // 双向→只写,隐式转换
    consumer(ch)      // 双向→只读,隐式转换
}

4.3 select 多路复用

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()
    go func() {
        time.Sleep(50 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    // select 随机选择一个就绪的 case
    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)  // 大概率先执行这个(ch2 更快)
    }
}

4.4 超时模式

func main() {
    ch := make(chan int)
    go func() {
        time.Sleep(2 * time.Second)
        ch <- 42
    }()

    select {
    case v := <-ch:
        fmt.Println("received:", v)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout!")  // 1秒超时
    }
}

4.5 done channel 模式

func main() {
    done := make(chan struct{})

    go func() {
        defer close(done) // ★ 函数退出时关闭,通知所有等待者
        // ... 长时间工作 ...
        time.Sleep(100 * time.Millisecond)
    }()

    <-done // 等待工作完成
    fmt.Println("goroutine finished")
}

4.6 扇出扇入模式(fan-out/fan-in)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d: job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan int, 10)

    // 扇出:3 个 worker 消费同一个 jobs 通道
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 发送任务
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // 扇入:收集所有结果
    for r := 1; r <= 9; r++ {
        fmt.Println("result:", <-results)
    }
}

5 并发安全专题详解

5.1 channel 本身的并发安全性

channel 操作本身是并发安全的——hchan 有 mutex 保护:

// 所有操作都需要获取锁
lock(&c.lock)
// ... 修改 hchan 字段 ...
unlock(&c.lock)

但 channel 不保证外部数据的并发安全

// ✅ channel 操作安全
ch <- value     // 安全
v := <-ch       // 安全

// ❌ 通过 channel 传递的共享数据可能不安全
m := map[string]int{}
ch <- &m        // 传递了 map 指针
// 如果两个 goroutine 同时修改 m → data race!

5.2 goroutine 泄漏的常见模式

// ❌ 泄漏模式1:无人接收
func leak1() {
    ch := make(chan int)
    go func() {
        ch <- 42  // 永远阻塞!没有人接收
    }()
    // 函数返回,ch 失去引用
    // 但 goroutine 仍然阻塞在 ch 上 → 泄漏
}

// ✅ 修复:使用 select + context
func noLeak1(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done():  // 超时或取消时退出
        }
    }()
}

// ❌ 泄漏模式2:无人发送
func leak2() {
    ch := make(chan int)
    go func() {
        v := <-ch  // 永远阻塞!没有人发送
        fmt.Println(v)
    }()
}

// ✅ 修复:close 或 select
func noLeak2() {
    ch := make(chan int)
    go func() {
        select {
        case v := <-ch:
            fmt.Println(v)
        case <-time.After(5 * time.Second):
            fmt.Println("timeout")
        }
    }()
}

6 经典问题与优化

6.1 遍历中删除/新增元素的风险

channel 的 for range 语义:

for v := range ch {
    // 循环直到 ch 被 close
    // close 是唯一退出 for range 的方式
}

// for range 编译为:
//   var hiter hiter
//   for mapiterinit(t, m, &hiter); hiter.key != nil; mapiternext(&hiter) { ... }
// channel 的 for range 编译为:
//   for {
//       v, ok := <-ch
//       if !ok { break }  // ch 已关闭且缓冲区空
//       // 使用 v
//   }

注意:channel 不像 map,for range 期间不允许关闭以外的修改(发送是安全的,但 close 必须在发送方)。

6.2 channel key 类型限制

channel 本身可以作为 map 的 key:

m := map[chan int]string{}  // ✅ chan 是引用类型,可比较(比较指针地址)
ch1 := make(chan int)
ch2 := make(chan int)
m[ch1] = "first"
m[ch2] = "second"
// ch1 == ch1 → true(同一指针)
// ch1 == ch2 → false(不同指针)

6.3 内存优化与最佳实践

// 1. 及时 close 不再使用的 channel(让接收者退出 for range)
close(ch)

// 2. 使用 buffered channel 减少阻塞
ch := make(chan int, runtime.NumCPU())  // 缓冲区大小 = CPU 核数

// 3. struct{} 零大小通道(纯信号,零内存开销)
done := make(chan struct{})
done <- struct{}{}
<-done

// 4. 使用 sync.Pool 复用 sudog(runtime 自动完成)

// 5. 避免 goroutine 泄漏——总是确保有退出路径

7 总结:Channel 核心知识图谱

Channel 核心知识

三种类型
无缓冲/有缓冲/nil

hchan 结构体
环形队列+等待队列

发送流程
直接→缓冲→阻塞

接收流程
直接→缓冲→阻塞

close 语义
唤醒所有等待者

跨栈拷贝
sendDirect/recvDirect

select 随机化
pollorder+lockorder

并发安全
hchan.mutex 保护

goroutine 泄漏
无人接收/无人发送

hchan 关键不变量一图总结

┌──────────────────────────────────────────────────────────┐
│                    hchan 状态机                            │
├────────────┬─────────────────┬───────────────────────────┤
│   状态      │   sendq          │   recvq                  │
├────────────┼─────────────────┼───────────────────────────┤
│ 缓冲区空    │ 空(可写入)     │ 有等待者(阻塞中)        │
│ 缓冲区部分  │ 空               │ 空                        │
│ 缓冲区满    │ 有等待者(阻塞中)│ 空(可读取)              │
│ 已关闭+空   │ 空(panic新发)  │ 空(返回零值)            │
│ 已关闭+有数据│ 空(panic新发)  │ 空(继续读缓冲区)        │
├────────────┼─────────────────┼───────────────────────────┤
│ nil 通道    │ 发送=永久阻塞    │ 接收=永久阻塞             │
└────────────┴─────────────────┴───────────────────────────┘

源码索引

文件 关键内容
runtime/chan.go:20 hchan 结构体、waitqmakechanchansendchanrecvclosechansendrecvsendDirectrecvDirect
runtime/select.go:116 selectgoscasesellock/selunlock、pollorder/lockorder

更多推荐