跳到主要内容

select 多路复用

问题

select 的执行规则是什么?如何用 select 实现超时控制?空 select 有什么用?

答案

select 语法与规则

select 是专为 channel 设计的多路复用语句:

select {
case v := <-ch1:
// ch1 可读
case ch2 <- val:
// ch2 可写
case <-time.After(5 * time.Second):
// 超时
default:
// 所有 channel 都不就绪时执行
}

核心规则

  1. 所有 case 的 channel 操作同时求值
  2. 多个 case 就绪时,随机选择一个
  3. 没有 case 就绪且无 default 时,阻塞等待
  4. 有 default 时,没有 case 就绪就执行 default(非阻塞)
  5. 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)
}
}

注意 breakfor + select 中只跳出 select,不跳出 for。需要用 label break 或 return。

相关链接