一句话结论

select 是 Go 的多路复用语句——同时监听多个 Channel 操作,哪个先就绪执行哪个。多个同时就绪则伪随机选择一个,避免饿死。配合 default 可实现非阻塞操作。

核心原理

执行机制

1. 对所有 case 的 Channel 上锁(按地址排序防死锁)
2. 检查是否有 case 就绪(可以立即执行而不阻塞)
3. 如果多个就绪 → 伪随机选一个
4. 如果都不就绪:
   a. 有 default → 执行 default
   b. 没有 default → 当前 Goroutine 挂到所有 Channel 的等待队列,解锁,挂起
5. 被唤醒后 → 执行就绪的 case

伪随机选择

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

for i := 0; i < 10; i++ {
    select {
    case <-ch1: fmt.Print("1")
    case <-ch2: fmt.Print("2")
    }
    // 输出类似: 1212121121 —— 不是严格的交替
    ch1 <- 1
    ch2 <- 2
}

Go 对就绪的 case 做了均匀洗牌(Fisher-Yates shuffle),每次 select 都重新洗——所以是伪随机而非真正的随机,但保证了公平性。

进阶用法

default 的风险

// ❌ CPU 100% 的典型原因
for {
    select {
    case msg := <-ch:
        process(msg)
    default:
        // 空转!没有任何阻塞,疯狂循环
    }
}

// ✅ 正确:去掉 default,让 select 阻塞等待
for msg := range ch {
    process(msg)
}

nil Channel 妙用:动态启用/禁用 case

var sendCh chan<- int  // nil
var recvCh <-chan int   // nil

for {
    select {
    case sendCh <- data:  // sendCh 为 nil 时这个 case 被禁用
    case data = <-recvCh: // recvCh 为 nil 时这个 case 被禁用
    }
    // 动态切换:想发送时 set sendCh,想接收时 set recvCh
}

超时控制

select {
case result := <-taskCh:
    fmt.Println("成功:", result)
case <-time.After(3 * time.Second):
    fmt.Println("超时")
}
// ⚠️ time.After 在超时前不会被 GC!高并发时避免在循环中用 time.After

改进: 使用 time.NewTimer + timer.Stop() 替代循环中的 time.After。

项目中的应用

在 物联网AI-Agent中枢控制平台 中,心跳检查用 select + time.Ticker:

func heartbeatLoop(ctx context.Context, ticker *time.Ticker) {
    for {
        select {
        case <-ticker.C:
            sendHeartbeat()
        case <-ctx.Done():
            ticker.Stop()
            return  // 优雅退出
        }
    }
}

在 AI-Agent工作流与评测平台 中,SSE 推送用 select 同时监听进度更新和客户端断开:

func streamProgress(ctx context.Context, taskID string, w http.ResponseWriter) {
    for {
        select {
        case progress := <-redisSub.Ch():
            fmt.Fprintf(w, "data: %s\n\n", progress)
        case <-ctx.Done():
            return
        }
    }
}

高频面试问题

Q: select 和 switch 有什么区别?

30 秒回答: switch 是顺序匹配值,select 是并发等待 Channel 操作。switch 的 case 顺序执行,select 的 case 随机选择(就绪时)。select 只能用于 Channel 操作。

Q: select 中既有发送又有接收,同时就绪怎么选?

30 秒回答: 伪随机,公平洗牌。不是代码里写在前面的优先。

常见错误

  1. for + select + default 空转 → CPU 100%

  2. 循环中 time.After → 内存泄漏(Timer 不回收)

  3. 忘记 ctx.Done() → Goroutine 无法退出

最小实验

// select 随机性验证
func main() {
    ch1 := make(chan struct{})
    ch2 := make(chan struct{})
    close(ch1)  // 已关闭=永远就绪
    close(ch2)
    
    count := map[int]int{}
    for i := 0; i < 1000; i++ {
        select {
        case <-ch1: count[1]++
        case <-ch2: count[2]++
        }
    }
    fmt.Println(count)  // {1: ~500, 2: ~500}
}

速记

select = 多路复用 + 伪随机。多就绪→公平洗牌。nil Chan → case 禁用。default → 非阻塞。time.After → 慎防内存泄漏。