go并发的核心数据结构--channel

发布时间:2026/7/31 5:44:22
go并发的核心数据结构--channel
开篇go并发的核心数据结构–channelgo语言并发的核心哲学是不要通过共享内存来通信而要通过通信来共享内存。channel通道就是go语言提供的协程间的通信方式用来传递数据、同步状态、协调并发从底层避免了共享内存带来的竞态问题是 Go 原生并发编程的标准方案。一、channel 基础语法1. 声明与创建仅声明nil通道读写都会永久阻塞varchchanint创建无缓冲通道(同步)ch:make(chanint)创建有缓冲通道(异步)先入先出ch:make(chanint,5)// 容量为5的有缓冲int通道2. 三大核心操作操作语法说明发送ch - value将数据写入通道接收value : -ch从通道读取数据关闭close(ch)关闭通道补充接收操作支持双返回值写法 value, ok : -chok 为 false 表示通道已关闭且缓冲区已空。二、核心分类无缓冲通道 vs 有缓冲通道这是 channel 最核心的分类两者的行为模式和适用场景完全不同。1. 无缓冲通道同步通道定义顾名思义无缓冲就是数据在经过通道时不能停止此通道创建时没有容量make(chan int)核心特性简单来说发送和接收的操作是同步完成一方没有就绪另一方就会阻塞。适用场景goroutine 协程之间的同步等待、一对一即时通信。下面看一个具体例子用无缓冲通道实现主 goroutine 等待子 goroutine 完成任务funcmain(){done:make(chanstruct{})// 空结构体不占用内存专用于传递信号gofunc(){// 子goroutine执行业务逻辑fmt.Println(子goroutine任务执行中...)time.Sleep(1*time.Second)fmt.Println(子goroutine任务完成)done-struct{}{}// 发送完成信号}()-done// 阻塞等待直到收到信号fmt.Println(主goroutine收到完成信号继续执行)}2. 有缓冲通道异步通道定义顾名思义有缓冲就是数据在经过通道时可以停留等待此通道创建时指定的缓冲区容量决定可以停留的数据个数make(chan int, 5)核心特性缓冲区空时可发送不可接收接收会阻塞缓冲区未满时可发送可接收不会阻塞缓冲区满时不可发送可接收发送会阻塞。适用场景生产者消费者解耦、并发限流、异步任务队列。下面看一个具体例子简单生产者消费者模型funcmain(){ch:make(chanint,3)// 容量为3的缓冲通道// 生产者发送数据gofunc(){fori:1;i5;i{ch-i fmt.Printf(生产者发送数据%d\n,i)}close(ch)// 发送完毕关闭通道}()// 消费者接收数据fornum:rangech{// for range 会持续监听ch管道直到ch管道关闭且缓冲区为空fmt.Printf(消费者收到数据%d\n,num)}fmt.Println(所有数据处理完成)}3. 单向通道补充一点Go 语言中默认的通道是双向通道可以进行发送和接收操作而为了提升代码安全性Go 支持单向通道限制通道在函数参数中只能发送或只能接收chan- T只写通道只能发送数据不能接收-chan T只读通道只能接收数据不能发送// 生产者函数参数为只写通道funcproducer(chchan-int){fori:0;i5;i{ch-i}close(ch)}// 消费者函数参数为只读通道funcconsumer(ch-chanint){fornum:rangech{fmt.Println(num)}}三、select 多路并发控制前面讲解了channle 通道的基本使用方法在这里会展示当需要同时监听多个通道的状态比如同时等待多个任务结果、处理多路信号时使用select可以同时监听多个通道的读写事件哪个通道就绪就执行对应分支是 Go 并发控制的核心语法。funcmain(){// 使用select case多路并发控制ch1:make(chanint)ch2:make(chanint)select{casech1-1:fmt.Println(ch1可写)caseval:-ch2:fmt.Println(ch2可读,val)default:fmt.Println(其它情况)}}官方规范就绪即执行哪个 case 的通道操作不阻塞就绪就执行对应分支。随机选择多个 case 同时就绪时随机选择一个执行不会按顺序优先匹配。default 非阻塞存在 default 分支时所有通道都阻塞则执行 default不会挂起 goroutine没有 default 则整体阻塞。nil 通道忽略nil 通道对应的 case 会永远阻塞相当于被排除在监听之外。禁用 fallthrough每个 case 独立不能穿透。小结channel 规范内置函数 len() 和 cap() 可以作用于通道分别查询缓冲区中数据的个数、缓冲区的大小。通道实现一种 FIFO先入先出的队列数据总是按照写入的顺序流出通道。goroutine 泄漏goroutine 因为永久阻塞无法退出占用的栈内存等资源无法释放长期运行会耗尽系统资源。1. 阻塞向 nil 通道发送或接收数据永久阻塞。无缓冲通道没有配对的接收 / 发送操作对应端永久阻塞。有缓冲通道缓冲区空接收端阻塞缓冲区满发送端阻塞。2. 通道关闭关闭操作只能由发送方执行接收方不应关闭通道。重复关闭已关闭的通道、关闭nil通道都会触发panic向已关闭的通道发送数据触发panic。向已关闭的通道接收数据如缓冲区有数据则正常读取无则返回对应类型零值双返回值的 ok 为false。四、实现原理1. 数据结构源码包中定义了通道的数据结构channel 的底层是 hchan 结构体typehchanstruct{qcountuint// 当前队列元素数量dataqsizuint// 环形缓冲区总容量make(chan T, N) 的Nbuf unsafe.Pointer// 环形缓冲区数组起始地址仅带缓冲chan才有elemsizeuint16// 单个元素占用字节大小closeduint16// 是否关闭0未关闭1已关闭elemtype*_type// 元素类型信息GC、赋值、大小sendxuint// 发送索引下一个存入buf的位置recvxuint// 接收索引下一个取出buf的位置// 等待队列双向链表阻塞的goroutinerecvq waitq// 等待接收的G队列-ch 阻塞的Gsendq waitq// 等待发送的G队列ch-x 阻塞的Glock mutex// 全局互斥锁保护所有字段并发修改}可以看到 hchan 结构体的字段还是比较多的但在这里我们也不是每个字段都需要深入了解。2. 无缓冲通道比如我们创建的通道是无缓冲 Channel(make(chan int))它没有缓冲区因此只需要注意recvq和sendq这两个字段。接着先执行发送操作这时如果没有对应接收操作那么当前协程G就会加入sendq阻塞等待接收 G 唤醒先执行接收操作这时如果没有对应发送操作那么当前协程G就会加入recvq阻塞等待发送 G 唤醒。3. 有缓冲通道而对于有缓冲 Channel(make(chan int, 5))则要复杂一些环形队列chan 内部实现了一个环形队列作为其缓冲区队列的长度dataqsiz是在创建 chan 时指定的。下图展示了一个可以缓存 5 个元素的通道图中的缓冲队列缓存了 qcount 2 个元素dataqsiz 5 代表缓冲队列可以缓存 5 个元素buf 则是指向这个缓冲队列的起始地址sendx 代表下一个写入数据的索引recvx 代表第一个读取数据的索引。写入数据向一个通道中写入数据的简单过程如下如果缓冲区中有空余位置则将数据写入缓冲区结束发送过程如果缓冲区中没有空余位置则将当前协程加入sendq队列进入睡眠并等待被读协程唤醒。但如果缓冲区为空且recvq队列中有协程在等待此时会将数据直接传递给recvq队列中的第一个协程而不必再写入缓冲区。读取数据从一个通道中读取数据的简单过程如下如果缓冲区中有数据则从缓冲区中取出数据结束读取过程如果缓冲区中没有数据则将当前协程加入recvq队列进入睡眠并等待被写协程唤醒。但如果没有缓冲区且sendq队列中有协程在等待此时会直接从sendq队列中的第一个协程中读取数据。关闭通道队列中有协程在等待此时会将数据直接传递给recvq队列中的第一个协程而不必再写入缓冲区。读取数据从一个通道中读取数据的简单过程如下如果缓冲区中有数据则从缓冲区中取出数据结束读取过程如果缓冲区中没有数据则将当前协程加入recvq队列进入睡眠并等待被写协程唤醒。但如果没有缓冲区且sendq队列中有协程在等待此时会直接从sendq队列中的第一个协程中读取数据。关闭通道会把recvq中的协程全部唤醒并返回对应数据的零值把sendq中协程全部唤醒但都会触发panic