--- tags: [go, golang, Select, 并发, 多路复用] create time: 2026-06-07 14:45 --- # Select ## 概述 `select` 是 Go 原生提供的多路复用机制,用于在多个 channel 操作中选择一个执行。它是构建超时控制、心跳检测、任务调度等并发模式的核心原语。 ## 正文 ### select 是什么? > [!question] 💭 思考 > 如果一个 goroutine 需要同时监听 3 个 channel,但一次只有一个有数据,怎么高效地处理而不阻塞? `select` 的语法类似于 `switch`,但每个 case 必须是 channel 操作: ```go 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 导致死锁 > ```go > func main() { > select {} // ❌ 没有任何 case,永久阻塞 > } > ``` > Go 运行时检测到所有 goroutine 都阻塞时会报 `deadlock` 错误。这是调试并发程序时最常见的 panic 之一。 ### 超时控制模式 > [!tip] 💡 最实用的 select 模式:超时控制 ```go // 方式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 会伪随机选择一个: ```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 读写: ```go select { case msg := <-ch: fmt.Println("收到消息:", msg) default: fmt.Println("当前没有待处理的消息") // 不会阻塞 } ``` 这在轮询场景或实现高性能服务端中非常有用。 ### 实际应用场景 #### 场景1:带超时的 RPC 调用 ```go 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:优雅关闭 ```go 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 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Channel]] - [[hzh/GolangStar/Go语言进阶/Context]] - [[hzh/GolangStar/Go语言进阶/定时器]]