Appearance
31|select 与超时
select 是 Go 并发编程里的多路复用器,能同时等待多个 channel 操作,哪个先就绪就执行哪个。它最常见的用途是:等待任务结果的同时设置超时,防止某个慢操作无限阻塞整个程序。配合 time.After 或 time.NewTimer,select 能给 channel 通信加上时间边界。
一、select 基础
select 同时等待多个 channel 操作:
go
package main
import (
"fmt"
"time"
)
func main() {
result := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
result <- "ok"
}()
select {
case value := <-result:
fmt.Println("result:", value)
case <time.After(1 * time.Second):
fmt.Println("timeout")
}
}select 会阻塞到某个 case 可以执行。上面例子里,如果 1 秒内 result 没有值,就会走 time.After 对应的超时分支;如果 1 秒内结果先回来,就会打印结果。
多个 case 同时就绪时,select 随机选择一个执行。这个随机性是有意设计的,避免某个 channel 永远被优先处理而导致饥饿。
二、time.After 的坑
time.After 适合小示例。长期运行的循环里频繁创建 time.After 会产生额外计时器对象,定时循环通常用 time.NewTimer 更好控制:
go
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
select {
case value := <-result:
fmt.Println("result:", value)
case <-timer.C:
fmt.Println("timeout")
}time.NewTimer 和 time.After 在 select 里的行为一样,区别是前者可以手动 Stop 和 Reset,避免循环中不断创建新计时器对象。
如果需要重复超时判断(比如每隔 5 秒检查一次任务状态),用 time.NewTicker:
go
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case value := <-result:
fmt.Println("result:", value)
return
case <-ticker.C:
fmt.Println("still waiting...")
}
}三、default 分支
select 加 default 后变成非阻塞:
go select
case value := <-result:
fmt.Println("result:", value)
default:
fmt.Println("no result yet")
}没有 case 就绪时,执行 default 然后立刻返回。这个模式常用于轮询或尝试发送而不阻塞。
四、用 nil channel 禁用 case
把某个 channel 设为 nil,对应的 select case 永远不会就绪,相当于禁用了这个分支:
go
var ch1 chan int = make(chan int)
var ch2 chan int
select {
case <-ch1:
fmt.Println("ch1")
case <-ch2: // ch2 是 nil,这个 case 永远不会执行
fmt.Println("ch2")
}这个技巧在动态调整监听对象时很有用。比如有多个任务通道,某个任务完成后把对应通道设为 nil,后续 select 不再监听它。
五、完整超时模式
批量巡检时,每个任务都要有独立的超时控制:
go
func checkWithTimeout(host string, timeout time.Duration) Result {
result := make(chan Result, 1)
go func() {
result <- check(host)
}()
select {
case r := <-result:
return r
case <time.After(timeout):
return Result{Host: host, OK: false, Message: "timeout"}
}
}每个任务启动一个 goroutine,用独立的 select 控制超时。这样一批任务里,个别目标卡住不会影响其他目标。
六、常见错误
select 里只有一个 case
go
select {
case v := <-ch:
fmt.Println(v)
}只有一个 case 的 select 和直接 <-ch 等价,但多了一个不必要的语法层。除非是为了和 default 搭配,否则不要这样写。
忘记停止 timer
go
for {
select {
case <-time.After(1 * time.Second):
// 每次循环都创建新计时器
}
}循环里用 time.After 会堆积大量未释放的计时器对象。改用 time.NewTimer 并 Reset:
go
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
for {
select {
case <-timer.C:
timer.Reset(1 * time.Second)
}
}超时后 goroutine 泄漏
go
func bad() {
result := make(chan int)
go func() {
time.Sleep(10 * time.Second)
result <- 42 // 超时后没人接收,goroutine 永远阻塞
}()
select {
case <-result:
case <time.After(1 * time.Second):
return // result 通道无缓冲,发送方永远阻塞
}
}超时退出后,发送方 goroutine 卡在 result <- 42 上。用有缓冲 channel(make(chan int, 1))或 context 取消来避免泄漏。