编程 Go 1.28 的 Timer 复制检测:Stop() 返回 false 之后,定时器为什么还在跑

2026-11-08 21:02:30

Go 1.28 的 Timer 复制检测:Stop() 返回 false 之后,定时器为什么还在跑

time.Timer 和 time.Ticker 的方法都是指针接收者:func (t *Timer) Stop() bool、func (t *Ticker) Reset(d Duration)。调用 t.Stop() 时,编译器自动取地址,所以哪怕 t 是一个按值复制出来的结构体字段,代码也能正常编译,Stop() 也照常返回一个 bool。这就是问题所在:很多人下意识把 Timer 当成一个可以随便赋值、塞进 map、按值传参的普通结构体。

变更出处:Go 1.28 dev 变更汇总,对应 CL f6ddbafcf0;接口参考 pkg.go.dev/time#Timer、Go 1.28 release notes。这是 Go 1.28 开发周期内的草案变更,最终行为以该版本实际输出为准。

Timer 里装的是运行时状态,不是值

Timer 内部除了暴露的 channel,还有一个 runtime 侧的 timer 结构,注册到当前 P 的 timer heap 上,堆里登记的条目指向的是原来那个对象的 timer 字段地址(Go 1.23 起 timer 重写为 per-P heap、timer.lock 取代全局锁,同步 timer channel 也可被 GC 回收)。

按值复制后,副本里的 timer 字段是另一块内存,但堆里那条记录仍然挂在原对象上。于是:

  • 副本.Stop() 去堆里摘自己那份,找不到对应条目,直接返回 false,原 timer 继续跑;
  • 副本.Reset() 把副本的 timer 重新加进堆,现在堆里有两份,各自到点向同一个 channel 发送;
  • 回调(或 channel 上的接收方)被触发两次,<-t.C 多出一次 tick;
  • 最麻烦的是「停不掉」:代码里写了 Stop(),返回值没人看,行为上却表现为定时器永不停止。

以前怎么排查

这类 bug 逃得过现有工具:go vet 的 copylocks 只认 sync.Locker/noCopy,Timer 不在名单里;race detector 看不到——复制本身不是数据竞争;pprof 的 goroutine 栈里也没有 timer,它不是一个 goroutine。旧实现用一个 init bool 标记是否初始化,复制之后副本的 init 是 true,看起来「已经初始化好了」,一切正常,更难察觉。

实际排查基本靠读代码:找 time.Timer 出现过的地方,看它是不是以值形式出现在 struct 字段、map value、函数返回值、函数参数里,尤其是 *time.NewTimer(d) 这种解引用写法,几乎必然是复制。

Go 1.28 改成自指针检测

Go 1.28 把内部的 init bool 换成了指向自身的指针:初始化时记下 &t,之后凡是调用 Stop/Reset 等方法,先比对这个自指针是不是仍等于当前对象的地址。按值复制出来的副本,自指针指向的是原对象,一比就露馅,于是会被检测到并报错。具体报什么、是 panic 还是别的形式,以该版本的实际输出为准。

需要强调的是:能检测到不等于自动修好。检测只是把静默的错误变成显式错误,代码仍然要改成 *time.Timer / *time.Ticker。

before / after

// before:Timer 按值存在 struct 里,构造时还解引用复制了一次
type job struct {
	timer time.Timer
}

func newJob(d time.Duration) job {
	return job{timer: *time.NewTimer(d)}
}

func (j job) stop() bool {
	return j.timer.Stop() // 操作的是副本,堆里那条还在跑
}
// after:持有指针,构造时直接用 NewTimer 的返回值
type job struct {
	timer *time.Timer
}

func newJob(d time.Duration) *job {
	return &job{timer: time.NewTimer(d)}
}

func (j *job) stop() bool {
	return j.timer.Stop()
}

Ticker 同理:不要 *time.NewTicker(d),不要把 time.Ticker 放进 map 的 value 或按值传参,统一用 time.NewTicker 的返回值并用 *time.Ticker 持有。

复制全文 生成海报 Go 定时器 并发

推荐文章

程序员茄子在线接单