Go语言select块跨channel读写的运行时处理逻辑疑问
问题解答
执行现象
该代码的核心问题是会无感知丢失ticker触发的事件,不会出现软锁,具体执行逻辑如下:
- Go语言规范明确规定:
select语句在判断分支就绪性前,会先按源码顺序对所有case的收发操作关联表达式进行全量求值。 - 代码中第一个分支
case triggerChan <- <-ticker.C:,会先执行<-ticker.C读取定时器的值(只要ticker.C有就绪值就会直接读走),再判断能不能写入triggerChan。 - 首次进入
select时triggerChan已经被写满(容量1,已塞入初始值),所以刚读取的ticker值无法写入,这个分支会被判定为未就绪。 - 此时第二个分支
case <-triggerChan:是就绪状态,select会选中该分支执行worker.DoWork(),但刚才从ticker.C读出来的定时器值会被直接丢弃,相当于这次定时触发直接漏了。
运行时的判断逻辑
- Go运行时不会把
triggerChan <- <-ticker.C这个组合操作当成原子操作判断就绪性,只会拆分成分立的「读ticker.C」和「写triggerChan」两个步骤分别处理,读操作完成后才会判断写操作是否就绪,所以不会提前识别到写操作会阻塞。 - 不会出现软锁:只要
triggerChan里有值,第二个分支就永远是就绪状态,select永远能选中该分支执行,不会卡住。
修复方案
需求为首次立即执行、后续每分钟执行一次,可直接使用更简洁的无额外channel方案实现,完全规避上述问题:
// 先立即执行一次 worker.DoWork() ticker := time.NewTicker(time.Minute) defer ticker.Stop() // 后续每次定时器触发执行 for range ticker.C { worker.DoWork() }
如果必须保留triggerChan的设计,只需要把ticker的读操作独立成单独的case即可,避免提前求值导致丢事件:
triggerChan := make(chan time.Time, 1) triggerChan <- time.Now() ticker := time.NewTicker(time.Minute) defer ticker.Stop() for { select { case t := <-ticker.C: // 非阻塞写入,避免triggerChan满时阻塞 select { case triggerChan <- t: default: // 已有待执行的触发,丢弃本次定时信号即可 } case <-triggerChan: worker.DoWork() } }
内容的提问来源于stack exchange,提问作者Zyl
相关产品推荐
相关产品推荐

