Skip to content

Go语言中的 select 语句 ​

select 让一次等待同时盯住多个通道:哪个通道先就绪,就先处理哪个的 case。它是 Go 并发里做多路复用的语法,time.After 超时、多路结果汇总都靠它。

select 语句的基本用法 ​

select 语句的语法和使用方法如下:

go
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 语句的特点 ​

  1. 选择执行:select 语句会选择一个可以立即执行的 case。如果多个 case 都可以执行,则随机选择一个执行。
  2. 阻塞行为:如果没有任何 case 可以执行,select 会阻塞,直到有一个 case 可以执行。
  3. 默认分支:如果存在 default 分支,并且所有通道都阻塞,则会立即执行 default 分支,而不会阻塞。

示例 ​

示例1:基本用法 ​

go
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:多个通道 ​

go
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 语句的工作流程 ​

  1. 初始化:当 select 语句执行时,Go 运行时会创建一个包含所有 case 的列表,并初始化相应的等待队列。
  2. 检查准备状态:Go 运行时会遍历所有的 case,检查每个通道的状态。如果发现某个 case 可以立即执行,则随机选择一个准备好的 case 执行,并跳过后续步骤。
  3. 阻塞等待:如果所有的 case 都不能立即执行,则将当前 Goroutine 加入所有相关通道的等待队列,并挂起当前 Goroutine。
  4. 唤醒执行:当某个通道变得可用时,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)):

go
// 伪代码:示意 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 实现,前面那段伪代码就是它的骨架。

参考资料 ​

相关页面 ​

Hello Golang