08.Golang 通道(channel )源码分析(二、数据发送与接收)
Golang 通道(channel )源码分析(二、数据发送与接收)
注意当前go版本代码为1.23
Channel 在运行时的内部表示是
runtime.hchan
介绍
当向 Channel 发送数据时,就需要使用 ch <- i 语句,编译器会将它解析成 OSEND 节点并在 cmd/compile/internal/gc.walkexpr 中转换成
而接收 Channel数据时,可以使用两种不同的方式去接收 Channel 中的数据:
1 | |
最终会通过编译器转换成 runtime.chanrecv1 和 runtime.chanrecv2 两种不同函数的调用,这两个函数最终会调用 runtime.chanrecv进行处理。
数据发送
1 | |
从上述代码可以知道,go通道发送主要流程总结如下:
发送流程:
- 空 Channel 检查 && 竞态检测 && (非阻塞)快速检测 && 加锁 && Channel 关闭检查:
- 三种发送情况:
- 情况 1: 存在等待的接收者 (
c.recvq.dequeue() != nil)- 从接收队列
c.recvq中取出一个等待的接收者sg。 - 调用
send(c, sg, ep, func() { unlock(&c.lock) }, 3)直接将数据从发送者ep传递给接收者sg,绕过缓冲区。 - 发送成功后唤醒接收者
sg。 - 发送成功,返回
true。
- 从接收队列
- 情况 2: 缓冲区有空间 (
c.qcount < c.dataqsiz)- 计算下一个可用缓冲区位置
qp。 - 如果启用了竞态检测,调用
racenotify通知将要向缓冲区写入数据。 - 使用
typedmemmove将数据从ep复制到缓冲区qp。 - 更新发送索引
c.sendx和缓冲区元素计数c.qcount。 - 释放锁
c.lock。 - 发送成功,返回
true。
- 计算下一个可用缓冲区位置
- 情况 3: 不存在缓冲区或者缓冲区已满且为阻塞模式 (
block == true)- 获取当前 goroutine 的指针
gp。 - 从
sudog池中分配一个sudog结构体mysg。 - 初始化
mysg的各个字段,包括要发送的数据指针ep、当前 goroutinegp、当前 channelc等。 - 将
mysg加入到 channel 的发送队列c.sendq中。 - 设置
gp.parkingOnChan为true,表示 goroutine 即将因 channel 操作阻塞。 - 调用
gopark阻塞当前 goroutine,等待被唤醒。 gopark会释放锁c.lock。- 被唤醒后,检查
gp.waiting是否仍然指向mysg,确保等待列表的完整性。 - 检查
mysg.success字段,判断发送是否成功。 - 清理
mysg并释放。 - 如果
closed为true(发送失败),且 channel 未关闭,则抛出 “spurious wakeup” 异常 - 返回
(true, success)。
- 获取当前 goroutine 的指针
- 情况 1: 存在等待的接收者 (
数据接收
1 | |
主要逻辑和情况处理:
- 空 Channel 检查 && 竞态检测 && (非阻塞)快速检测 && 加锁 && Channel 关闭检查:
- 通道未关闭:
- 情况 1: 存在等待的发送者 (
c.sendq不为空):- 调用recv函数接收数据,如果 Channel 不存在缓冲区,调用 runtime.recvDirect 将 Channel 发送队列中 Goroutine 存储的 elem 数据拷贝到目标内存地址中;如果 Channel 存在缓冲区;将发送队列头的数据拷贝到缓冲区中,释放一个阻塞的发送方;
- 情况 2: 缓冲区中有数据 (
c.qcount > 0):- 直接从缓冲区
c.recvx索引处读取数据。 - 如果
ep不为nil,则将数据拷贝到ep指向的内存。 - 更新
c.recvx和c.qcount。 - 返回
(true, true)。
- 直接从缓冲区
- 情况 3: 不存在缓冲区或者缓冲区为空且为阻塞模式:
- 获取当前 goroutine 的指针
gp。 - 从
sudog池中分配一个sudog结构体mysg。 - 初始化
mysg的各个字段,包括要发送的数据指针ep、当前 goroutinegp、当前 channelc等。 - 将
mysg加入到 channel 的发送队列c.recvq中。 - 调用
gopark函数阻塞当前 goroutine,等待被唤醒。 - 被唤醒后,检查
gp.waiting是否仍然指向mysg,确保等待列表的完整性。 - 检查
mysg.success字段,判断发送是否成功。 - 清理
mysg并释放。 - 返回
(true, success)。
- 获取当前 goroutine 的指针
- 情况 1: 存在等待的发送者 (
问题
| 操作 | nil channel | closed channel | not nil, not closed channel |
|---|---|---|---|
| close | panic | panic | 正常关闭 |
| 读 <- ch | 阻塞 | 读到对应类型的零值 | 阻塞或正常读取数据。缓冲型 channel 为空或非缓冲型 channel 没有等待发送者时会阻塞 |
| 写 ch <- | 阻塞 | panic | 阻塞或正常写入数据。非缓冲型 channel 没有等待接收者或缓冲型 channel buf 满时会被阻塞 |
1.Channel 发送和接收数据的本质是什么?
All transfer of value on the go channels happens with the copy of value.
就是说 channel 的发送和接收操作本质上都是 “值的拷贝”,无论是从 sender goroutine 的栈到 chan buf,还是从 chan buf 到 receiver goroutine,或者是直接从 sender goroutine 到 receiver goroutine。
1 | |
1 | |

参考链接
2.《Go学习笔记》
08.Golang 通道(channel )源码分析(二、数据发送与接收)
https://blog.longpi1.com/2024/12/14/08-Golang-通道(channel-)源码分析(二、数据发送与接收)/