Go语言中的 select 语句
select 让一次等待同时盯住多个通道:哪个通道先就绪,就先处理哪个的 case。它是 Go 并发里做多路复用的语法,time.After 超时、多路结果汇总都靠它。
select 语句的基本用法
select 语句的语法和使用方法如下:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout")
}
}在这个例子中,ch1、ch2 和 time.After 三个 case 里,谁先就绪就先执行谁,其余的留给下一轮。具体规则见下面的特点列表。
select 语句的特点
- 选择执行:
select语句会选择一个可以立即执行的case。如果多个case都可以执行,则随机选择一个执行。 - 阻塞行为:如果没有任何
case可以执行,select会阻塞,直到有一个case可以执行。 - 默认分支:如果存在
default分支,并且所有通道都阻塞,则会立即执行default分支,而不会阻塞。
示例
示例1:基本用法
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "message"
}()
select {
case msg := <-ch:
fmt.Println("Received:", msg)
case <-time.After(1 * time.Second):
fmt.Println("Timeout")
}
}在这个示例中,由于 ch 需要等待2秒钟才有数据,而 time.After(1 * time.Second) 只等待1秒钟,因此会输出 "Timeout"。
示例2:多个通道
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "one"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "two"
}()
select {
case msg1 := <-ch1:
fmt.Println("Received", msg1)
case msg2 := <-ch2:
fmt.Println("Received", msg2)
case <-time.After(3 * time.Second):
fmt.Println("Timeout")
}
}在这个示例中,select 会选择 ch1 或 ch2 中第一个准备好的通道进行处理,因此会输出 "Received one"。
select 语句的底层实现原理
调度器和 Goroutine
Go语言的并发模型基于 Goroutine 和调度器。Goroutine 是一种轻量级线程,由 Go 运行时管理。调度器负责管理所有的 Goroutine,并在操作系统线程之间调度它们。
select 语句的工作流程
- 初始化:当
select语句执行时,Go 运行时会创建一个包含所有case的列表,并初始化相应的等待队列。 - 检查准备状态:Go 运行时会遍历所有的
case,检查每个通道的状态。如果发现某个case可以立即执行,则随机选择一个准备好的case执行,并跳过后续步骤。 - 阻塞等待:如果所有的
case都不能立即执行,则将当前 Goroutine 加入所有相关通道的等待队列,并挂起当前 Goroutine。 - 唤醒执行:当某个通道变得可用时,Go 运行时会唤醒等待在该通道上的所有 Goroutine,并再次检查哪个
case可以执行。唤醒的 Goroutine 会继续执行select语句,并选择一个可以执行的case。
代码分析
下面是 select 底层核心逻辑的简化示意伪代码(不是真实源码;真实实现在 runtime/select.go 的 selectgo 函数中,签名为 func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool)):
// 伪代码:示意 select 的整体流程
func selectSelect(cases []scase, block bool) (int, bool) {
ncases := len(cases)
// 1. 遍历所有 case,检查是否有可以立即执行的
var ready []int
for i := 0; i < ncases; i++ {
if caseCanProceed(&cases[i]) {
ready = append(ready, i)
}
}
if len(ready) > 0 {
// 2. 多个就绪时随机选择一个,保证公平
return ready[fastrand()%uint32(len(ready))], true
}
if !block {
return -1, false // 有 default 分支时直接返回
}
// 3. 把当前 goroutine 依次加入每个 case 对应通道的等待队列,然后挂起
gopark(selectGoPark, "select")
// 4. 被某个通道唤醒后,由唤醒方记录命中的 case 序号并返回
return parkResult(), true
}结论
select 的行为可以归结为三条:多个 case 同时就绪时随机挑一个,避免总被同一路通道占住;没有就绪的就挂起当前 goroutine,等任意一个通道有动静再醒;写了 default 就不挂起,直接走 default 返回。这套逻辑在运行时里由 runtime/select.go 的 selectgo 实现,前面那段伪代码就是它的骨架。