go select 原理解析
概述#
go 的 select
语句是专门为了 channel 发送和接收消息而诞生的专用语句(不要和 switch
搞混了), 在语句的运行期间, 该 goroutine 是阻塞的.
select 在 golang 的语言层提供了I/O 多路复用
, 可以同时检测多个 channel
I/O 多路复用#
有必要了解一下 I/O 多路复用概念
在不使用 select 时, 如果要监听 N 个 channel. 对于普通的多线程处理, 可能需要启动 N 个 goroutine, 每个 goroutine 监听一个 channel, 这样的缺点显而易见: 系统需要额外的创建和维护goroutine, 因为大多数时候, channel 都会阻塞, 只有少部分会接受到数据
而使用 select 时, 可以做到在一个 goroutine 里监听多个 channel, 系统只需要维护一个 goroutine, N 个 channel 都依靠这一个 goroutine 进行数据的"运输", 当其中某一个 channel 有数据时, 根据对应的 channel 走不同的流程, 无需对额外的 goroutine 进行管理, 无疑提高了效率
当然, select 也不要无节制的使用, 最好是在逻辑上有一定的关联性, 否则会破坏代码的可读性.
demo#
举个例子
func main() {
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
select {
case v := <-ch1:
// 如果ch1通道成功读取数据,则执行该case处理语句
fmt.Printf("ch1 = %v", v)
case v := <-ch2:
// 如果ch2通道成功读取数据,则执行该case处理语句
fmt.Printf("ch2 = %v", v)
default:
// 如果上面case都没有成功,则进入default处理流程
// 如果没有 default, 会一直阻塞等待某个 case 成功
fmt.Println("default!")
}
}
注意两个问题:
- select 并不是一个循环, 如果你需要反复的监听多个 channel, 搭配
for{}
使用 - default 作用是当 case 都不成功时, 立刻进入 default 流程, 结束 select, 如果你需要阻塞住, 就不要使用 default
- 当搭配
for{}
反复的执行 slelct 时, 如非业务要求, 否则不要使用 default, 会造成select 立即退出后重新循环
所以, 常用的方式如下
for {
select {
case v := <-ch1:
// 如果ch1通道成功读取数据,则执行该case处理语句
fmt.Printf("ch1 = %v", v)
case v := <-ch2:
// 如果ch2通道成功读取数据,则执行该case处理语句
fmt.Printf("ch2 = %v", v)
}
// 一次读取之后立刻再次监听
}
数据结构#
select 底层由两部分组成, case 语句和执行函数
每一个 case 语句结构如下
type scase struct {
c *hchan // chan
elem unsafe.Pointer // 读或者写的缓冲区地址
}
这里的 hchan, 存放了监听的 channel, 在一个 select 中, 包含了多个 case. 这些 case 组成了一个数组
selectgo#
而执行的 select 语句, 实际上调用了函数func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
参数意义如下:
- cas0: case 数组中第一个case的地址
- order0: case数组两倍长的缓冲区
- ncases: case 数组的长度
selectgo 返回的说所选的 scase 的索引, 而如果 scase 是接收操作, 则返回是否收到值
流程#
我们在运行一个 select 时, 函数的调用顺序如下
func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)
func rselect([]runtimeSelect) (chosen int, recvOK bool) func
selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool)
前两个都是简单的初始化参数, 重点其实就在selectgo
里
selectgo 的处理流程如下:
- 根据 cas0 获取 case 数组
- 将 case 数组顺序打乱
- 将 case 数组内的每个 chan 全部上锁
- 遍历所有的 case 数组元素, 查看其是否可读和可写
- 如果有可读或可写 case, 解锁所有的 chan, 返回对应的 chan 数据
- 如果没有可读或可写, 有 defalut, 解锁所有的 chan, 返回 default 对应的 case
- 如果两者都没有, 则将当前的 goroutine 阻塞, 将当前 goroutine的 G加入到case 数组内的所有 chan 的等待队列中, 然后所有 chan 解锁
- 如果其中有一个 chan 可读或者可写时, 并且轮到这个 G 进行操作时, 将 goroutine 唤醒
- 执行步骤3-7
作者:chnmig
出处:https://www.cnblogs.com/chnmig/p/16744190.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南