是,但仅限于select语句本身不挂起goroutine——它不代表channel“空”或“满”,只是当前时刻所有case都不可立即执行时立刻跳进default;无default时select会永久阻塞。

select 里加 default 就是非阻塞?
是,但仅限于 select 语句本身不挂起 goroutine——它不代表 channel “空”或“满”,只是当前时刻所有 case 都不可立即执行时,立刻跳进 default。没 default 的 select 在无就绪 channel 时会永久阻塞(除非在 goroutine 内被其他逻辑唤醒)。
常见错误现象:select { case v := 单独一个可读 <code>case 却卡死;加了 default 后又总走 default,误以为 channel 没数据,其实是其他 goroutine 恰好没在写、或缓冲刚被消费完。
-
default不探测状态,只响应就绪性:channel 有数据但没人往里写(无缓冲)、或刚被另一个 goroutine 消费走一个元素,都可能导致本该读到的值“消失”在两次检查之间 - 读操作必须用
v, ok := 形式,否则无法区分“没数据”和“channel 已关闭” - 写操作同理:
ch 在 <code>case中是安全的,channel 关闭会 panic,但select会先判断可写性,不会 panic
非阻塞 recv / send 的标准写法
Go 官方推荐且并发安全的方式就是 select + default,不是 len(ch) == 0 或 len(ch) == cap(ch) ——这些值在多 goroutine 下读取后瞬间就可能失效。
典型发送场景(如日志打点、指标上报):
立即学习“go语言免费学习笔记(深入)”;
select {
case logCh <- entry:
// 成功发出
default:
// 缓冲满或接收端僵死,丢弃或降级
log.Warn("logCh full, dropped")
}典型接收场景(如心跳检测、事件轮询):
select {
case msg, ok := <-eventCh:
if ok {
handle(msg)
}
default:
// 无新事件,执行保活或清理逻辑
keepAlive()
}- 对无缓冲 channel,
default触发 = 当前无 goroutine 正在<-ch等待 - 对有缓冲 channel,
default触发 = 缓冲区已满(写)或为空(读),但注意:这仍是瞬时快照 - 别在
default里循环重试写入,那等于忙等;真要重试,得加time.Sleep或用指数退避
default 和 time.After 混用为什么总失败
因为 default 在每次 select 开始时都“立刻可执行”,而 <-time.After(100 * time.Millisecond) 是个延迟 channel,100ms 后才可读——只要没别的 case 就绪,default 永远赢。
错误写法:
select {
case v := <-ch:
...
case <-time.After(100 * time.Millisecond):
log.Println("timeout")
default:
log.Println("immediately fallback") // 这行永远先执行
}- 想实现“立即试一次,不行就等超时”:拆成两步,先
select+default尝试,失败后再跑一个不含default的select等time.After - 想实现“最多等 100ms,有数据就立刻返回”:去掉
default,只留ch和time.After两个case -
default和time.After语义冲突,不能共存于同一select
default 分支里偷偷阻塞是最隐蔽的坑
default 本身不阻塞,但它里面的代码可以。很多人写了 default 就以为整段逻辑非阻塞,结果在里面调 http.Get、db.Query 或 time.Sleep,整个 goroutine 又卡住了。
- 如果
default里需要耗时操作,必须显式用go func() { ... }()脱离当前流程 - 更稳妥的做法是把任务发到带缓冲的 channel,由专用 worker 处理,而不是在
select分支里硬扛 -
default和所有case共享变量作用域,注意别意外覆盖同名变量(比如在多个case和default里都声明err)
真正难的是对“非阻塞”的边界保持清醒:它只保证 select 这一行不挂起,不保证你后续写的任何东西都不挂起。


















