【Go 1.26.4】Golang Channel 深度解析
Golang Channel 深度解析
基于 Go 1.26.4 源码,源码路径:
github.com/go-go1.26.4
核心源文件:runtime/chan.go、runtime/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 适用场景
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 → 满
环形缓冲区的优势:避免频繁的内存分配和释放。数据直接在预分配的 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
}
三种路径:
┌─────────────────┬──────────────────┬───────────────────┐
│ 直接发送 │ 写入缓冲区 │ 阻塞等待 │
│ (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) // ★ 唤醒发送者
}

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 语义总结:
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_) // 跨栈内存拷贝
}
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 核心知识图谱
hchan 关键不变量一图总结:
┌──────────────────────────────────────────────────────────┐
│ hchan 状态机 │
├────────────┬─────────────────┬───────────────────────────┤
│ 状态 │ sendq │ recvq │
├────────────┼─────────────────┼───────────────────────────┤
│ 缓冲区空 │ 空(可写入) │ 有等待者(阻塞中) │
│ 缓冲区部分 │ 空 │ 空 │
│ 缓冲区满 │ 有等待者(阻塞中)│ 空(可读取) │
│ 已关闭+空 │ 空(panic新发) │ 空(返回零值) │
│ 已关闭+有数据│ 空(panic新发) │ 空(继续读缓冲区) │
├────────────┼─────────────────┼───────────────────────────┤
│ nil 通道 │ 发送=永久阻塞 │ 接收=永久阻塞 │
└────────────┴─────────────────┴───────────────────────────┘
源码索引
文件 关键内容 runtime/chan.go:20hchan结构体、waitq、makechan、chansend、chanrecv、closechan、send、recv、sendDirect、recvDirectruntime/select.go:116selectgo、scase、sellock/selunlock、pollorder/lockorder
更多推荐

所有评论(0)