千言万语讲不出一句再见
一、Go Channel 底层实现核心逻辑( Go 1.21 版本)
Go 的 chan(通道)是实现 goroutine 间安全通信的核心组件,底层由编译器 + 运行时(runtime) 实现,核心设计思路是:
- 数据结构:用环形队列存储数据,结合互斥锁(mutex)保证并发安全,用等待队列(sendq/recvq)阻塞 / 唤醒读写 goroutine;
- 核心操作:
-
- 发送(ch <- val):编译器转为 runtime.chansend,无缓冲 / 缓冲区满则阻塞,否则入队;
-
- 接收(val <- ch/val, ok := <-ch):编译器转为 runtime.chanrecv,无缓冲 / 缓冲区空则阻塞,否则出队;
-
- 关闭(close(ch)):编译器转为 runtime.closechan,唤醒所有等待的 goroutine 并标记通道关闭;
- 核心特性:FIFO 顺序、并发安全、阻塞 / 非阻塞模式、关闭后读返回零值。
二、Go 源码核心结构体与函数
1. chan 核心结构体(runtime/chan.go)
// hchan 是 channel 的底层结构体,所有 chan 操作都围绕它展开
type hchan struct {
qcount uint // 环形队列中已存储的元素个数
dataqsiz uint // 环形队列的容量(缓冲区大小,如 make(chan int, 10) 则为10)
buf unsafe.Pointer // 环形队列的起始地址(指向元素数组)
elemsize uint16 // 通道中每个元素的字节大小(如 int 是8字节)
closed uint32 // 标记通道是否关闭:0=未关闭,1=已关闭
elemtype *_type // 通道元素的类型信息(如 int/string/自定义结构体)
sendx uint // 环形队列的发送索引(下一个发送元素的位置)
recvx uint // 环形队列的接收索引(下一个接收元素的位置)
recvq waitq // 等待接收的 goroutine 队列(阻塞的读goroutine)
sendq waitq // 等待发送的 goroutine 队列(阻塞的写goroutine)
// 互斥锁:保护 hchan 所有字段的并发访问(核心!保证chan线程安全)
lock mutex
}
// waitq 是 goroutine 等待队列,存储阻塞的 goroutine 链表
type waitq struct {
first *sudog // 队列头
last *sudog // 队列尾
}
// sudog 是 goroutine 的封装,记录 goroutine 阻塞的相关信息
type sudog struct {
g *g // 关联的 goroutine
next *sudog // 链表下一个节点
prev *sudog // 链表上一个节点
elem unsafe.Pointer // 指向 chan 发送/接收的元素地址
// ... 其他字段省略(如阻塞原因、是否加锁等)
}
核心注解:
- hchan 是 chan 的 “真身”:make(chan int, 10) 本质是分配一个 hchan 结构体,初始化环形队列、锁、等待队列等;
- 环形队列:buf 指向缓冲区数组,sendx/recvx 标记读写位置,实现 FIFO;
- 等待队列:recvq 存 “想读但没数据” 的 goroutine,sendq 存 “想写但缓冲区满 / 无缓冲” 的 goroutine;
- lock:所有 chan 操作(读 / 写 / 关闭)都要先加锁,保证并发安全。
2. chan 创建:makechan(runtime/chan.go)
// makechan 是 make(chan T, size) 的底层实现,创建并初始化 hchan 结构体
// 参数:t 是 chan 类型信息,size 是缓冲区大小
// 返回值:hchan 指针(即用户层的 chan 变量)
func makechan(t *chantype, size int) *hchan {
// 1. 校验参数:元素大小、类型合法性
elem := t.elem
if elem.size >= 1<<16 {
throw("chan element size too large") // 元素不能超过64KB
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("bad alignment") // 内存对齐校验
}
// 2. 计算需要分配的总内存:hchan结构体 + 缓冲区(如果有)
var mem, buf unsafe.Pointer
totalSize := uintptr(0)
if size > 0 && elem.size > 0 {
// 有缓冲区:计算缓冲区内存(元素大小 * 容量)
totalSize = uintptr(size) * elem.size
// 分配缓冲区内存(按元素类型对齐)
buf = mallocgc(totalSize, elem, true)
}
// 3. 分配 hchan 结构体内存
mem = mallocgc(hchanSize, nil, true)
// 类型转换为 hchan 指针
c := (*hchan)(mem)
// 4. 初始化 hchan 字段
c.buf = buf // 缓冲区地址
c.elemsize = uint16(elem.size) // 元素大小
c.elemtype = elem // 元素类型
c.dataqsiz = uint(size) // 缓冲区容量
// 其他字段默认初始化:qcount=0, closed=0, sendx=0, recvx=0 等
return c
}
注解:
- 用户层 ch := make(chan int, 5) 会被编译器转为 makechan(chan int 类型信息, 5);
- 内存分配逻辑:先分配 hchan 结构体,再分配缓冲区(如果 size>0);
- 无缓冲 chan:size=0,buf=nil,dataqsiz=0。
3. chan 发送:chansend(runtime/chan.go)
// chansend 是 ch <- val 的底层实现,发送元素到通道
// 参数:c 是 hchan 指针,ep 是要发送的元素地址,block 是否阻塞(true=阻塞,false=非阻塞)
// 返回值:true=发送成功,false=非阻塞模式下发送失败
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
// 1. 快速失败:通道为 nil,直接 panic
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 2. 非阻塞模式 + 无缓冲区/缓冲区满 + 无等待接收的goroutine → 直接返回失败
if !block && c.closed == 0 && full(c) {
return false
}
// 3. 加锁(核心!所有chan操作必须加锁)
lock(&c.lock)
// 4. 校验:向已关闭的通道发送 → panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 5. 尝试唤醒等待接收的goroutine(优先“直接交付”,不走缓冲区)
if sg := c.recvq.dequeue(); sg != nil {
// 有goroutine在等接收 → 直接把元素交给它,不走缓冲区
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 6. 缓冲区有空间 → 把元素写入缓冲区
if c.qcount < c.dataqsiz {
// 计算元素要写入的位置(buf + sendx * 元素大小)
qp := chanbuf(c, c.sendx)
// 拷贝元素到缓冲区(从ep地址拷贝到qp地址)
typedmemmove(c.elemtype, qp, ep)
c.sendx++ // 发送索引后移
if c.sendx == c.dataqsiz { // 到缓冲区末尾,重置为0(环形队列)
c.sendx = 0
}
c.qcount++ // 已存储元素数+1
unlock(&c.lock) // 解锁
return true
}
// 7. 缓冲区满/无缓冲 → 阻塞当前goroutine
if !block {
unlock(&c.lock)
return false
}
// 8. 封装当前goroutine为sudog,加入sendq等待队列
gp := getg()
sg := acquireSudog()
sg.g = gp
sg.elem = ep
sg.c = c
// 入队sendq
c.sendq.enqueue(sg)
// 阻塞当前goroutine(解锁+挂起,等待被唤醒)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 被唤醒后清理
if sg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
sg.c = nil
releaseSudog(sg)
// 检查是否因通道关闭被唤醒(此时发送会panic)
if gp.param != nil {
panic(plainError("send on closed channel"))
}
unlock(&c.lock)
return true
}
// full 判断通道是否已满(无缓冲=满,有缓冲=qcount==dataqsiz)
func full(c *hchan) bool {
if c.dataqsiz == 0 {
return true // 无缓冲通道永远“满”(必须有接收者才能发送)
}
return c.qcount == c.dataqsiz // 有缓冲通道:元素数=容量则满
}
// send 直接将元素交付给等待的接收goroutine(不走缓冲区)
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 拷贝元素到接收goroutine的目标地址
if sg.elem != nil {
typedmemmove(c.elemtype, sg.elem, ep)
}
// 解锁(通过回调函数)
unlockf()
// 唤醒接收的goroutine
goready(sg.g, skip+1)
}
核心注解:
- 发送优先级:先唤醒等待的接收者(直接交付)→ 再写入缓冲区 → 最后阻塞自己;
- 直接交付:无缓冲 chan 的核心逻辑(ch <- val 必须等有 <-ch 才能完成);
- 阻塞逻辑:当前 goroutine 封装为 sudog 加入 sendq,调用 gopark 挂起,等待被接收者唤醒;
- 已关闭 chan 发送:加锁后检查 closed,直接 panic(这是 Go 的语法规则)。
4. chan 接收:chanrecv(runtime/chan.go)
// chanrecv 是 <-ch / val, ok := <-ch 的底层实现,从通道接收元素
// 参数:c 是 hchan 指针,ep 是接收元素的目标地址,block 是否阻塞
// 返回值:received=true(成功接收),closed=true(通道已关闭)
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (received, closed bool) {
// 1. 快速失败:通道为 nil → 阻塞(非阻塞返回false)
if c == nil {
if !block {
return false, false
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 2. 非阻塞模式 + 无元素 + 无等待发送的goroutine → 返回失败
if !block && empty(c) {
// 检查通道是否关闭
if atomic.Load(&c.closed) == 0 {
return false, false
}
// 通道已关闭且无元素 → 返回零值+closed=true
if empty(c) {
if ep != nil {
typedmemclr(c.elemtype, ep) // 填充零值
}
return false, true
}
}
// 3. 加锁
lock(&c.lock)
// 4. 通道已关闭且无元素 → 返回零值
if c.closed != 0 && c.qcount == 0 {
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep) // 填充元素类型的零值(如int=0,string="")
}
return false, true
}
// 5. 尝试唤醒等待发送的goroutine(直接接收,不走缓冲区)
if sg := c.sendq.dequeue(); sg != nil {
// 有goroutine在等发送 → 直接接收它的元素
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, false
}
// 6. 缓冲区有元素 → 从缓冲区读取
if c.qcount > 0 {
// 计算要读取的位置(buf + recvx * 元素大小)
qp := chanbuf(c, c.recvx)
if ep != nil {
// 拷贝元素到接收地址ep
typedmemmove(c.elemtype, ep, qp)
}
// 清空缓冲区该位置(避免内存泄漏)
typedmemclr(c.elemtype, qp)
c.recvx++ // 接收索引后移
if c.recvx == c.dataqsiz { // 环形队列重置
c.recvx = 0
}
c.qcount-- // 已存储元素数-1
unlock(&c.lock) // 解锁
return true, false
}
// 7. 无元素 → 阻塞当前goroutine
if !block {
unlock(&c.lock)
return false, false
}
// 8. 封装为sudog,加入recvq等待队列
gp := getg()
sg := acquireSudog()
sg.g = gp
sg.elem = ep
sg.c = c
c.recvq.enqueue(sg)
// 挂起goroutine,等待被发送者唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 被唤醒后清理
gp.waiting = nil
sg.c = nil
releaseSudog(sg)
// 检查是否因通道关闭被唤醒
closed = gp.param != nil
unlock(&c.lock)
return true, closed
}
// empty 判断通道是否为空(无缓冲=空,有缓冲=qcount==0)
func empty(c *hchan) bool {
return c.qcount == 0
}
// recv 直接从等待的发送goroutine接收元素(不走缓冲区)
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 从发送goroutine的元素地址拷贝到接收地址
if ep != nil {
typedmemmove(c.elemtype, ep, sg.elem)
}
unlockf()
// 唤醒发送的goroutine
goready(sg.g, skip+1)
}
核心注解:
- 接收优先级:先唤醒等待的发送者(直接接收)→ 再从缓冲区读取 → 最后阻塞自己;
- 关闭 chan 接收:通道关闭且无元素时,返回零值 + closed=true(用户层 val, ok := <-ch 的 ok 就是这个);
- 零值填充:typedmemclr 按元素类型填充零值(如结构体零值、指针 nil 等)。
5. chan 关闭:closechan(runtime/chan.go)
// closechan 是 close(ch) 的底层实现,关闭通道
func closechan(c *hchan) {
// 1. 快速失败:通道为 nil → panic
if c == nil {
panic(plainError("close of nil channel"))
}
// 2. 加锁
lock(&c.lock)
// 3. 校验:关闭已关闭的通道 → panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// 4. 标记通道为已关闭
c.closed = 1
// 5. 唤醒所有等待的goroutine
var glist gList
// 先唤醒所有接收者(recvq)
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
// 填充零值到接收地址
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
}
// 标记为“因关闭被唤醒”
sg.g.param = unsafe.Pointer(&closedchan)
glist.push(sg.g)
}
// 再唤醒所有发送者(sendq,这些goroutine会panic)
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.g.param = unsafe.Pointer(&closedchan)
glist.push(sg.g)
}
unlock(&c.lock)
// 6. 批量唤醒goroutine
for !glist.empty() {
g := glist.pop()
goready(g, 3)
}
}
核心注解:
- 关闭规则:nil chan 关闭 panic,重复关闭 panic;
- 唤醒逻辑:关闭后,所有阻塞的读 / 写 goroutine 都会被唤醒 —— 读 goroutine 拿到零值,写 goroutine 继续执行时会 panic;
- 内存可见性:c.closed 是原子操作,保证其他 goroutine 能快速感知通道关闭。
三、chan 核心操作流程可视化(Mermaid 流程图)
1. 通道发送(ch <- val)流程
2. 通道接收(val <- ch)流程
3. 通道关闭(close (ch))流程
四、关键细节
1. 无缓冲 chan vs 有缓冲 chan
- 无缓冲 chan:dataqsiz=0,buf=nil,发送必须等接收(直接交付),接收必须等发送;
- 有缓冲 chan:先写缓冲区,缓冲区满才阻塞发送者;先读缓冲区,缓冲区空才阻塞接收者;
- 核心区别:无缓冲 chan 是 “同步通信”,有缓冲 chan 是 “异步通信”(缓冲区做缓冲)。
2. 并发安全的底层保障
- 所有 chan 操作都加 lock 互斥锁,保证同一时间只有一个 goroutine 操作 chan;
- closed 字段用原子操作(atomic.Load),避免竞态;
- 等待队列的入队 / 出队操作都在锁内完成,保证 goroutine 调度安全。
3. 零值与关闭的关系
- 通道关闭后,所有后续接收都会返回零值(val, ok := <-ch 中 ok=false);
- 零值不是 “关闭的标志”:未关闭的 chan 也可能返回零值(如发送了 0/"" 等),必须通过 ok 判断是否关闭。
4. 性能优化点
- 环形队列:缓冲区用数组实现环形队列,避免数据拷贝(只需移动索引);
- 直接交付:优先唤醒等待的 goroutine,不走缓冲区,减少内存操作;
- sudog 复用:acquireSudog/releaseSudog 复用 sudog 结构体,减少内存分配。
总结
- 核心结构:chan 底层是 hchan 结构体,包含环形缓冲区、互斥锁、读写等待队列,实现并发安全的 FIFO 通信;
- 核心操作:
-
- 发送:优先直接交付接收者 → 再写缓冲区 → 最后阻塞;
-
- 接收:优先直接接收发送者 → 再读缓冲区 → 最后阻塞;
-
- 关闭:标记状态 + 唤醒所有等待者,读返回零值,写触发 panic;
- 核心特性:无缓冲同步、有缓冲异步、并发安全、关闭后读零值;
- 关键规则:nil chan 读写阻塞、关闭 nil / 已关闭 chan panic、发送到已关闭 chan panic。
简单记:chan 是 “带锁的环形队列 + 等待队列”,核心是 “直接交付优先,缓冲区次之,阻塞兜底”,保证 goroutine 间安全通信。