Golang Channel 原理深度解析
1. 前言
Channel 是 Golang 在语言层面提供的 goroutine 间的通信方式,设计理念是:
"不要通过共享内存来通信,而应该通过通信来共享内存"
2. 数据结构概述
2.1 hchan 结构体
go
type hchan struct {
qcount uint // 当前队列中已有的元素数量
dataqsiz uint // 环形队列的容量(make(chan int, n) 中的 n)
buf unsafe.Pointer // 指向环形队列底层数组的指针
elemsize uint16 // 单个元素的大小(字节数)
closed uint32 // channel 是否已关闭(0 未关闭,1 已关闭)
timer *timer // timer feeding this chan
elemtype *_type // 元素的类型信息,用于反射和 GC
sendx uint // 发送操作的索引位置(队尾)
recvx uint // 接收操作的索引位置(队头)
recvq waitq // 因接收而阻塞的 goroutine 队列
sendq waitq // 因发送而阻塞的 goroutine 队列
bubble *synctestBubble
// lock 保护 hchan 所有字段
lock mutex
}
2.2 核心组成
环形队列
| 字段 | 说明 |
|---|---|
dataqsiz |
队列长度(容量) |
buf |
指向队列内存 |
qcount |
队列中当前元素数 |
sendx |
写入位置索引 [0, dataqsiz) |
recvx |
读取位置索引 [0, dataqsiz) |
等待队列
| 队列 | 作用 |
|---|---|
recvq |
因接收而阻塞的 goroutine 队列 |
sendq |
因发送而阻塞的 goroutine 队列 |
被阻塞的 goroutine 会被挂在 channel 中的等待队列中,该等待队列的数据结构是一个双向链表。
类型信息
一个 channel 只能传递一种类型的信息,类型信息存放在 hchan 数据结构中:
elemtype:元素类型,用于数据传递过程中的赋值elemsize:单个元素大小,用于在 buf 中定位元素位置
3. Channel 创建
3.1 创建流程伪代码
go
// 伪代码:makechan(t *chantype, size int) *hchan
FUNCTION makechan(type_info, buffer_size):
// 1. 参数校验
elem_type = type_info.Elem
IF elem_type.size >= 65536: // 1 << 16
THROW "元素大小超过限制"
// 2. 计算所需内存
required_mem = elem_type.size * buffer_size
IF overflow OR required_mem > MAX_ALLOC OR buffer_size < 0:
PANIC "size out of range"
// 3. 根据不同情况分配内存
SWITCH:
CASE required_mem == 0: // 无缓冲 chan 或元素大小为 0
chan = allocate(hchan_size)
chan.buf = chan地址 // 指向自己,避免 nil
CASE elem_type.has_pointer == false: // 元素不含指针
// 一次性分配 hchan + buffer,减少 GC 压力
chan = allocate(hchan_size + required_mem)
chan.buf = chan地址 + hchan_size
DEFAULT: // 元素包含指针
// 分别分配,便于 GC 扫描
chan = allocate(hchan_size)
chan.buf = allocate(required_mem)
// 4. 初始化字段
chan.elemsize = elem_type.size
chan.elemtype = elem_type
chan.dataqsiz = buffer_size // 循环队列容量
chan.qcount = 0 // 当前元素数
chan.sendx = 0 // 发送索引
chan.recvx = 0 // 接收索引
chan.closed = 0 // 未关闭
chan.sendq = empty // 发送等待队列
chan.recvq = empty // 接收等待队列
// 5. 初始化锁
INIT_LOCK(chan.lock)
RETURN chan
3.2 内存分配策略
| 场景 | 分配方式 | 原因 |
|---|---|---|
| 无缓冲/零大小元素 | 单独分配 hchan | buf 不需要实际内存 |
| 元素不含指针 | hchan + buf 一起分配 | 减少 GC 扫描对象 |
| 元素含指针 | 分别分配 | GC 需要扫描 buf 中的指针 |
4. 向 Channel 写入数据
4.1 写入流程图

4.2 核心代码
go
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 1. nil channel 处理
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceBlockForever, 2)
throw("unreachable")
}
// 2. 快速路径:非阻塞且满
if !block && c.closed == 0 && full(c) {
return false
}
// 加锁
lock(&c.lock)
// 3. 向已关闭的 channel 发送数据 → panic
if c.closed != 0 {
unlock(&c.lock)
panic("send on closed channel")
}
// 4. 优先:有等待的接收者 → 直接传递
if sg := c.recvq.dequeue(); sg != nil {
// 直接向等待的 sg 发送数据
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 5. 缓冲区未满 → 写入
if c.qcount < c.dataqsiz {
qp := chanbuf(c, c.sendx)
typedmemmove(c.elemtype, qp, ep)
c.sendx++
// 移动到队头,底层数据结构是循环队列
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++
unlock(&c.lock)
return true
}
// 6. 非阻塞 → 返回 false
if !block {
unlock(&c.lock)
return false
}
// 7. 阻塞:加入 sendq,挂起
gp := getg()
mysg := acquireSudog()
mysg.elem.set(ep)
mysg.g = gp
mysg.c.set(c)
c.sendq.enqueue(mysg)
// 修改协程运行状态,挂起当前 goroutine
// 协程运行状态:
// 1. _Grunnable 1 就绪队列,等待被调度
// 2. _Grunning 2 正在被执行
// 3. _Gwaiting 4 阻塞等待
// 4. _Gsyscall 3 执行系统调用
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
// 被唤醒后继续执行...
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
closed := !mysg.success
mysg.c.set(nil)
releaseSudog(mysg)
if closed {
panic("send on closed channel")
}
return true
}
4.3 写入流程总结
| 优先级 | 条件 | 操作 |
|---|---|---|
| 1 | recvq 有等待者 | 直接传递数据,唤醒接收者 |
| 2 | 缓冲区有空位 | 写入缓冲区,sendx++ |
| 3 | 非阻塞 | 返回 false |
| 4 | 阻塞 | 加入 sendq,调用 gopark 挂起 |
5. 从 Channel 读取数据
5.1 读取流程图

5.2 核心代码
go
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 1. nil channel 处理
if c == nil {
// 非阻塞读取,直接返回
if !block {
return
}
// 永久阻塞
gopark(nil, nil, waitReasonChanReceiveNilChan, traceBlockForever, 2)
throw("unreachable")
}
// 2. 快速路径:非阻塞且空
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
return
}
// channel 已不可逆地关闭,重新检查是否有待处理数据
if empty(c) {
// channel 已关闭且为空
if ep != nil {
typedmemclr(c.elemtype, ep) // 返回类型零值
}
return true, false
}
}
// 加锁
lock(&c.lock)
// 3. channel 已关闭检查
if c.closed != 0 {
// 关闭的 channel 内没有数据了
if c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep) // 返回零值
}
return true, false
}
// channel 已关闭,但缓冲区还有数据
} else {
// 4. 优先:从 sendq 等待队列中获取数据
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
// 5. 从缓冲区读取数据
if c.qcount > 0 {
qp := chanbuf(c, c.recvx)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
unlock(&c.lock)
return true, true
}
// 6. 非阻塞读取,直接返回
if !block {
unlock(&c.lock)
return false, false
}
// 7. 阻塞等待被唤醒
gp := getg()
mysg := acquireSudog()
mysg.elem.set(ep)
mysg.g = gp
mysg.c.set(c)
// 加入等待队列
c.recvq.enqueue(mysg)
// 挂起当前 goroutine
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceBlockChanRecv, 2)
// 被唤醒后继续执行...
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
success := mysg.success
mysg.c.set(nil)
releaseSudog(mysg)
return true, success
}
5.3 读取流程总结
| 优先级 | 条件 | 操作 |
|---|---|---|
| 1 | channel 已关闭且空 | 返回零值,received=false |
| 2 | sendq 有等待者 | 直接传递数据,唤醒发送者 |
| 3 | 缓冲区有数据 | 从缓冲区读取,recvx++ |
| 4 | 非阻塞 | 返回 (false, false) |
| 5 | 阻塞 | 加入 recvq,调用 gopark 挂起 |
6. 常见用法
6.1 range 遍历
通过 range 可以持续从 channel 中读出数据,好像在遍历一个数组一样。
go
func chanRange(chanName chan int) {
for e := range chanName {
fmt.Printf("Get element from chan: %d\n", e)
}
}
特点:
- 当 channel 中没有数据时会阻塞当前 goroutine
- channel 关闭后自动退出循环
- 如果发送方未关闭 channel,range 将永久阻塞
6.2 select 多路复用
select 本质上也是阻塞读取且是随机执行,也就是在 send、recv 函数中 block 参数为 true。
go
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
ch3 := make(chan string)
// 模拟三个异步任务
go func() {
time.Sleep(3 * time.Second)
ch1 <- "任务1完成(3秒)"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "任务2完成(1秒)"
}()
go func() {
time.Sleep(2 * time.Second)
ch3 <- "任务3完成(2秒)"
}()
start := time.Now()
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case msg := <-ch3:
fmt.Println(msg)
}
fmt.Printf("耗时: %v\n", time.Since(start))
}
输出(不唯一,取决于哪个先完成):
任务2完成(1秒)
耗时: 1s
6.3 阻塞与非阻塞
只有在 select 中有 default 语句时,send、recv 中的 block 参数才为 false。
go
// 带 default 的 select → 非阻塞
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
return chanrecv(c, elem, false) // block = false
}
// 不带 default 的 select → 阻塞
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true) // block = true
}
| 写法 | block 参数 | 行为 |
|---|---|---|
v := <-ch |
true |
阻塞 |
for v := range ch |
true |
阻塞 |
select { case <-ch: } |
true |
阻塞 |
select { case <-ch: default: } |
false |
非阻塞 |
7. gopark 与 goready
7.1 gopark - 挂起 goroutine
gopark 是 Go 运行时中挂起当前 goroutine 的核心函数。
go
func gopark(unlockf func(*g, unsafe.Pointer) bool,
lock unsafe.Pointer,
reason waitReason,
traceReason traceBlockReason,
traceskip int)
作用: 将当前 goroutine 从 _Grunning 状态切换到 _Gwaiting 状态,让出 CPU 给其他 goroutine 使用。
7.2 goready - 唤醒 goroutine
goready 用于唤醒被挂起的 goroutine。
go
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
作用: 将 goroutine 从 _Gwaiting 状态切换到 _Grunnable 状态,重新加入调度队列。
7.3 状态转换
gopark goready
│ │
▼ ▼
_Grunning ──────────► _Gwaiting _Gwaiting ──────────► _Grunnable
▲ ▲
│ │
└────────── 继续执行 ◄────────────┘
(被调度器重新调度)
重要: goready 是由唤醒方(如接收者)调用的,不是被唤醒方(发送者)自己调用的。
8. 总结
8.1 Channel 行为速查表
| 场景 | 行为 |
|---|---|
读写 nil channel |
永久阻塞 |
| 向已关闭的 channel 发送 | panic |
| 关闭已关闭的 channel | panic |
| 从已关闭的 channel 读取 | 返回零值,received=false |
| 从已关闭且有数据的 channel 读取 | 正常读取数据 |
| 无缓冲 channel | 直接传递,无缓冲区 |
| 有缓冲 channel | 缓冲区满/空时阻塞 |
select { case <-ch: default: } |
唯一的非阻塞操作 |
8.2 核心要点
- 数据传递优先级:等待队列 > 缓冲区 > 阻塞
- 直接传递:有发送者和接收者同时存在时,数据直接传递,最高效
- 环形队列:缓冲区使用环形队列实现,sendx 和 recvx 循环移动
- 锁保护:所有字段由 lock 保护,并发安全
- 非阻塞操作 :只有带
default的 select 才是非阻塞的
参考源码
src/runtime/chan.go- Channel 核心实现src/runtime/proc.go- gopark/goready 调度实现src/runtime/runtime2.go- goroutine 状态定义