select 语句
本教程共 80 篇 · 第 58 篇 · 更新于 2026-07-27 · 约 7 分钟阅读
58. select 语句
本节目标:学会用
select同时监听多个 channel,掌握随机选择、default 非阻塞和超时模式。
多路复用
当你有多个 channel 要同时盯,用 select。它的语法和 switch 很像,但 case 只能是 channel 操作:
select {
case v := <-ch1:
fmt.Println("从 ch1 收到:", v)
case v := <-ch2:
fmt.Println("从 ch2 收到:", v)
}
select 会阻塞,直到某一个 case 可以执行。如果多个 case 同时就绪,会随机挑一个执行,不是按顺序。
随机选择的意义
为什么是随机而不是从上到下?为了避免某个 channel 总是饥饿。如果按顺序,第一个就绪的 channel 永远优先,后面的可能一直轮不到。随机选择让每个 channel 都有机会。
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
ch1 <- "A"
}()
go func() {
ch2 <- "B"
}()
for i := 0; i < 2; i++ {
select {
case v := <-ch1:
fmt.Println(v)
case v := <-ch2:
fmt.Println(v)
}
}
}
每次运行,先打印 A 还是 B 不一定。
配合 for 循环
单次 select 只执行一个 case。要持续监听,套一层 for:
for {
select {
case v := <-ch1:
fmt.Println("ch1:", v)
case v := <-ch2:
fmt.Println("ch2:", v)
case <-time.After(time.Second):
fmt.Println("超时退出")
return
}
}
这是 Go 里最常见的消息循环写法。
default 非阻塞
没有 default 时,所有 case 都不就绪就阻塞。加上 default,不就绪就走默认分支,不阻塞:
select {
case v := <-ch:
fmt.Println("收到:", v)
default:
fmt.Println("没数据,干别的去")
}
利用这点可以实现非阻塞的发送和接收:
// 非阻塞发送
func trySend(ch chan<- int, v int) bool {
select {
case ch <- v:
return true
default:
return false
}
}
// 非阻塞接收
func tryRecv(ch <-chan int) (int, bool) {
select {
case v := <-ch:
return v, true
default:
return 0, false
}
}
Tip检查 context 是否已取消也可以非阻塞:
select {
case <-ctx.Done():
// 已取消
default:
// 未取消
}
超时模式
time.After 返回一个只读 channel,指定时间后会往里发一个值。配合 select 实现超时:
func main() {
ch := make(chan int)
go func() {
time.Sleep(2 * time.Second)
ch <- 42
}()
select {
case v := <-ch:
fmt.Println("收到:", v)
case <-time.After(time.Second):
fmt.Println("超时了,不等了")
}
}
这个模式很常用,比如 HTTP 请求超时控制、任务限时执行。
Warning
time.After每次调用都会创建一个新的 timer。如果在高频循环里用,可能导致 timer 堆积。长时间运行的服务建议用time.NewTimer手动管理,或者用context.WithTimeout。
nil channel 会被跳过
select 的 case 如果操作的是 nil channel,不会阻塞,而是直接被跳过:
var nilCh chan int // nil
select {
case <-nilCh:
fmt.Println("这行永远不会执行")
case <-time.After(time.Second):
fmt.Println("timeout")
}
利用这个特性,可以在运行时动态启用或禁用某个 case:把 channel 设成 nil 就禁用,赋个真 channel 就启用。
永久阻塞
空的 select{} 会永久阻塞,因为没有 case 也没有 default:
func main() {
fmt.Println("start")
select {}
fmt.Println("这行永远执行不到")
}
偶尔有用,比如让主 goroutine 卡住等信号。
小结
select同时监听多个 channel,谁先就绪执行谁- 多个就绪时随机选一个,避免饥饿
default让select非阻塞time.After配合select实现超时nilchannel 的case会被跳过- 空
select{}永久阻塞
下一节讲 channel 的关闭和 range 遍历。