Golang Channel 原理深度解析

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 核心要点

  1. 数据传递优先级:等待队列 > 缓冲区 > 阻塞
  2. 直接传递:有发送者和接收者同时存在时,数据直接传递,最高效
  3. 环形队列:缓冲区使用环形队列实现,sendx 和 recvx 循环移动
  4. 锁保护:所有字段由 lock 保护,并发安全
  5. 非阻塞操作 :只有带 default 的 select 才是非阻塞的

参考源码

  • src/runtime/chan.go - Channel 核心实现
  • src/runtime/proc.go - gopark/goready 调度实现
  • src/runtime/runtime2.go - goroutine 状态定义
相关推荐
源代码•宸1 分钟前
Golang基础语法(go语言指针、go语言方法、go语言接口、go语言断言)
开发语言·经验分享·后端·golang·接口·指针·方法
Bony-2 分钟前
Golang 常用工具
开发语言·后端·golang
Paul_09202 分钟前
golang编程题
开发语言·算法·golang
ONLYOFFICE2 分钟前
入门指南:远程运行 ONLYOFFICE 协作空间 MCP 服务器
运维·服务器·github·onlyoffice
携欢2 分钟前
portswigger靶场之修改序列化数据类型通关秘籍
android·前端·网络·安全
csbysj20202 分钟前
Go 语言变量作用域
开发语言
牛奔5 分钟前
GVM:Go 版本管理器安装与使用指南
开发语言·后端·golang
颜酱7 分钟前
用填充表格法-继续吃透完全背包及其变形
前端·后端·算法
百***78757 分钟前
2026 优化版 GPT-5.2 国内稳定调用指南:API 中转实操与成本优化
开发语言·人工智能·python
Dovis(誓平步青云)9 分钟前
《Linux 核心 IO 模型深析(中篇):探索Cmake与多路转接的高效实现poll》
linux·运维·服务器·数据库·csdn成长记录