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

159 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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语言进阶/定时器]]