select 多路复用
问题
select 的执行规则是什么?如何用 select 实现超时控制?空 select 有什么用?
答案
select 语法与规则
select 是专为 channel 设计的多路复用语句:
select {
case v := <-ch1:
// ch1 可读
case ch2 <- val:
// ch2 可写
case <-time.After(5 * time.Second):
// 超时
default:
// 所有 channel 都不就绪时执行
}
核心规则:
- 所有 case 的 channel 操作同时求值
- 多个 case 就绪时,随机选择一个
- 没有 case 就绪且无 default 时,阻塞等待
- 有 default 时,没有 case 就绪就执行 default(非阻塞)
- nil channel 的 case 永远不会被选中
常用模式
// 1. 超时控制
func fetchWithTimeout(ch <-chan Result) (Result, error) {
select {
case result := <-ch:
return result, nil
case <-time.After(3 * time.Second):
return Result{}, errors.New("timeout")
}
}
// 2. 非阻塞操作
select {
case msg := <-ch:
process(msg)
default:
// ch 没数据,不阻塞
}
// 3. 优雅退出
func worker(ctx context.Context, tasks <-chan Task) {
for {
select {
case <-ctx.Done():
return // 收到取消信号
case task := <-tasks:
task.Process()
}
}
}
// 4. 定时器 + 退出
func periodicTask(ctx context.Context) {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
doPeriodicWork()
}
}
}
// 5. 空 select(永久阻塞)
select {} // 常用于 main 中保持程序运行
select 优先级问题
select 是随机选择就绪的 case,如果需要优先级,可以用嵌套 select:
// 优先处理退出信号
for {
select {
case <-done:
return
default:
}
select {
case <-done:
return
case task := <-tasks:
process(task)
}
}
常见面试问题
Q1: select 中的 time.After 有什么问题?
答案:
在循环中使用 time.After 会每次循环创建新的 Timer,导致内存泄漏(Timer 在触发前不会被 GC):
// ❌ 内存泄漏
for {
select {
case msg := <-ch:
process(msg)
case <-time.After(5 * time.Second): // 每次循环创建新 Timer
return
}
}
// ✅ 复用 Timer
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()
for {
select {
case msg := <-ch:
if !timer.Stop() {
<-timer.C
}
timer.Reset(5 * time.Second)
process(msg)
case <-timer.C:
return
}
}
Q2: 如何实现 select 的循环监听?
答案:
用 for + select:
for {
select {
case <-ctx.Done():
return
case msg := <-ch:
process(msg)
}
}
注意 break 在 for + select 中只跳出 select,不跳出 for。需要用 label break 或 return。