You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言select块跨channel读写的运行时处理逻辑疑问

问题解答

执行现象

该代码的核心问题是会无感知丢失ticker触发的事件,不会出现软锁,具体执行逻辑如下:

  1. Go语言规范明确规定:select语句在判断分支就绪性前,会先按源码顺序对所有case的收发操作关联表达式进行全量求值。
  2. 代码中第一个分支case triggerChan <- <-ticker.C:,会先执行<-ticker.C读取定时器的值(只要ticker.C有就绪值就会直接读走),再判断能不能写入triggerChan。
  3. 首次进入select时triggerChan已经被写满(容量1,已塞入初始值),所以刚读取的ticker值无法写入,这个分支会被判定为未就绪。
  4. 此时第二个分支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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.13 20:51:03