default仅在所有case均阻塞时执行,并非非阻塞轮询;无缓冲channel需收发双方同时就绪,带缓冲channel满时发送阻塞、空时接收阻塞;default中避免耗时逻辑;time.after()不可复用。

select 里 default 不等于“没超时就走 default”
很多人以为 select 带 default 就是“非阻塞轮询”,其实不是。只要任一 case 准备就绪(比如 channel 已有值、或能立即发送),select 就会执行那个 case,default 完全不参与竞争。它只在所有 case 都阻塞时才触发。
常见错误现象:select 一直走 default,但实际 channel 里早有数据——大概率是 channel 被另一个 goroutine 占着没读/写完,或用了无缓冲 channel 却没配对操作。
- 用无缓冲
chan int时,send和recv必须同时就绪,否则任一端都会阻塞 - 带缓冲 channel 的
len(ch) == cap(ch)时,send也会阻塞;len(ch) == 0时,recv会阻塞 -
default分支里别放耗时逻辑,否则会掩盖真实调度意图,让轮询节奏失控
time.After() 在 select 中不能直接复用
time.After() 每次调用都新建一个 Timer,返回单次 。如果把它赋给变量后反复塞进多个 <code>select,第二次起就永远收不到信号——因为第一次 select 后 channel 已被消费,后续读取会永久阻塞(除非用 select { case 绕开)。
正确做法是每次 select 前重新调用 time.After(),或改用 time.NewTimer() 手动控制:
立即学习“go语言免费学习笔记(深入)”;
timer := time.NewTimer(100 * time.Millisecond)
defer timer.Stop()
// ...
select {
case <-ch:
// 处理
case <-timer.C:
// 超时
}
timer.Reset(100 * time.Millisecond) // 下一轮前重置-
time.AfterFunc()不适合 select 场景,它只触发一次回调,不提供可选 channel - 高频轮询中频繁创建
time.After()会有小量 GC 压力,但通常可忽略;真要极致优化才换Timer - 注意
timer.Reset()对已停止或已触发的 timer 返回 false,需检查返回值避免误判
select + for 实现轮询时,别漏掉 break label
嵌套 for 和 select 时,break 默认只跳出 select,不会终止外层循环。结果就是“超时一次就无限跑 default”,根本停不下来。
典型场景:想每 200ms 查一次状态,超时则重试,成功则退出整个轮询。
- 必须用带标签的
break,例如outer:+break outer - 用
return更干脆,尤其在函数内部;但若逻辑复杂需收尾,标签 break 更可控 - 别依赖
select的随机性来“假装”轮询公平——它只是伪随机,不能替代权重或优先级调度
channel 关闭后 select 仍可能 panic 或逻辑错乱
关闭的 channel 可以安全接收(返回零值+false),但不能发送。如果 select 的某个 case 是向已关闭的 channel 发送,运行时直接 panic:send on closed channel。
更隐蔽的问题是:关闭 channel 后,未被消费的缓存值仍存在,select 可能继续从里面读出旧数据,导致业务状态错位。
- 发送前先用
select { case ch 判断是否可发,但治标不治本 - 真正健壮的做法是:用额外的
done chan struct{}控制生命周期,而不是依赖 channel 关闭信号 - 不要在多个 goroutine 里并发关同一个 channel;关之前确保所有 sender 都已退出
轮询逻辑越简单越好,一旦加了重试、退避、多路聚合,就该考虑用 context.WithTimeout() 或封装成状态机——select 本身不维护上下文,硬堆容易漏边界。










