Files
cs-note/hzh/GolangStar/Go语言进阶/Select.md
T

4.1 KiB
Raw Blame History

tags, create time
tags create time
go
golang
Select
并发
多路复用
2026-06-07 14:45

Select

概述

select 是 Go 原生提供的多路复用机制,用于在多个 channel 操作中选择一个执行。它是构建超时控制、心跳检测、任务调度等并发模式的核心原语。

正文

select 是什么?

[!question] 💭 思考 如果一个 goroutine 需要同时监听 3 个 channel,但一次只有一个有数据,怎么高效地处理而不阻塞?

select 的语法类似于 switch,但每个 case 必须是 channel 操作:

select {
case msg := <-ch1:     // ch1 可读时执行
    fmt.Println("received", msg)
case ch2 <- data:      // ch2 可写时执行
    fmt.Println("sent")
default:               // 所有 case 都无法立即执行时
    fmt.Println("no channel ready")
}

核心行为规则:

  1. 所有 case 的 channel 操作都会被求值
  2. 如果有多个 case 同时就绪,随机选择一个执行
  3. 没有 case 就绪且有 default,立即执行 default
  4. 没有 case 就绪且无 default,阻塞等待

[!warning] ⚠️ 致命陷阱:空 select 导致死锁

func main() {
    select {} // ❌ 没有任何 case,永久阻塞
}

Go 运行时检测到所有 goroutine 都阻塞时会报 deadlock 错误。这是调试并发程序时最常见的 panic 之一。

超时控制模式

[!tip] 💡 最实用的 select 模式:超时控制

// 方式1:time.After(简洁)
select {
case result := <-doWork():
    fmt.Println("完成:", result)
case <-time.After(5 * time.Second):
    fmt.Println("超时!")
}

// 方式2:time.Timer(可取消,推荐)
timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // 提前返回时释放资源

select {
case result := <-doWork():
    fmt.Println("完成:", result)
    timer.Stop() // 完成后停止定时器
case <-timer.C:
    fmt.Println("超时!")
}

[!note] 📝 Timer vs After

  • time.After(d) 内部创建一个 Timer,无法提前释放——超时期间定时器仍在运行
  • time.NewTimer(d) 可随时调用 .Stop() 释放底层资源,适合长生命周期场景

随机选择

当多个 case 同时就绪时,Go 会伪随机选择一个:

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2

select {
case v := <-ch1:
    fmt.Println("ch1:", v) // 可能是 ch1 或 ch2
case v := <-ch2:
    fmt.Println("ch2:", v) // 结果不确定
}

[!info] ℹ️ 补充 这种随机性是为了避免"饥饿"——总是优先选择同一个 channel 会导致其他 channel 永远得不到服务。如果你需要确定性顺序,应使用其他方式编排。

非阻塞检查

利用 default 实现非阻塞的 channel 读写:

select {
case msg := <-ch:
    fmt.Println("收到消息:", msg)
default:
    fmt.Println("当前没有待处理的消息") // 不会阻塞
}

这在轮询场景或实现高性能服务端中非常有用。

实际应用场景

场景1:带超时的 RPC 调用

func callRPC(ctx context.Context, query string) (string, error) {
    resultCh := make(chan string, 1)
    
    go func() {
        resultCh <- doRemoteCall(query)
    }()
    
    select {
    case result := <-resultCh:
        return result, nil
    case <-ctx.Done():
        return "", ctx.Err() // 被取消
    }
}

场景2:优雅关闭

func monitor(ctx context.Context, ticker <-chan time.Time) {
    for {
        select {
        case t := <-ticker:
            fmt.Println("heartbeat at", t)
        case <-ctx.Done():
            fmt.Println("shutting down...")
            return
        }
    }
}

[!warning] ⚠️ 并发安全提醒

  • select 本身是并发安全的,多个 goroutine 可以同时执行不同的 select
  • 但不要在一个 goroutine 中对同一 channel 既读又写(除非你清楚自己在做什么)
  • 永远记得:关闭未初始化的 nil channel 和重复关闭都会 panic

关联笔记