Go Ticker 为什么停止后仍然被旧引用持有
来源:17golang原创
时间:2026-10-26 00:19:39 349浏览 收藏
我第一次遇到这个问题时,表面现象很像 time.Ticker.Stop() 没有生效:停止方法已经调用了,服务对象里却还能看到旧的 ticker 指针,接收 goroutine 也没有自己退出。真正的原因通常不是 ticker “拒绝释放”,而是把四件事混在了一起:停止发送、关闭通道、解除调用方引用,以及让垃圾回收器看到对象已经不可达。
Stop只停止后续 tick,不关闭ticker.C,也不会把调用方保存的指针自动改成nil。- 只要结构体字段、闭包或接收 goroutine 仍然持有旧 ticker 或旧 channel,旧引用就仍然存在;Go 1.23 的 GC 也不会替你消除业务引用。
- 需要停止的是业务循环时,要让接收方同时监听
ticker.C和context.Done(),再由拥有者负责 Stop、退出和清空字段。
Stop 停止了 tick,为什么旧引用还在
官方定义很明确:Ticker.Stop 会关闭 ticker 的计时动作,之后不再发送 tick,但它不会关闭 C 通道。这个设计是为了避免并发接收方因为通道被关闭而误判出一个“结束事件”,也保留了后续调用 Reset 的可能。
因此下面三句话的含义完全不同:
| 动作 | 它改变什么 | 它不会替你做什么 |
|---|---|---|
ticker.Stop() | 停止后续 tick | 不关闭 C,不清空调用方变量 |
close(ticker.C) | 尝试关闭通道 | 不应该由调用方执行,C 是只读通道且 ticker 仍可能被其他代码使用 |
ticker = nil | 移除当前变量的一条引用 | 不影响其他字段、闭包或 goroutine 保存的引用 |
| GC 回收 | 回收已经不可达的对象 | 不负责终止仍在运行的业务循环 |
所以,调用 Stop 后看到旧指针并不矛盾。停止是 ticker 的运行语义,引用是否还在则是调用方的所有权问题。

最容易漏掉的是被阻塞的接收 goroutine
旧引用经常不是单独放在一个变量里,而是藏在接收循环的闭包中。下面的写法可以收到 tick,但停止时没有第二个退出信号:
func startPolling() *time.Ticker {
ticker := time.NewTicker(time.Second)
go func() {
for {
// 这里只监听 ticker.C;Stop 不会关闭这个通道。
即使其他地方调用了 ticker.Stop(),这个 goroutine 仍可能停在下一次接收上。它保存着 ticker 的闭包引用,而循环本身又没有返回条件,于是你会同时观察到两个现象:旧引用还在,goroutine 也没有结束。
这不是给 Stop 再加一层包装就能解决的。停止计时和退出业务循环必须有各自的信号。
让 ticker.C 和退出信号进入同一个 select
我更愿意把 ticker 的所有权放在一个短生命周期的服务对象里,再用 context.Context 表达“这段工作应该结束”。收到退出信号后,接收 goroutine 自己返回,拥有者再清理字段:
type Poller struct {
ticker *time.Ticker
}
func (p *Poller) Run(ctx context.Context) {
p.ticker = time.NewTicker(time.Second)
defer func() {
// Run 返回前停止计时;Stop 不负责关闭 ticker.C。
p.ticker.Stop()
// 只有确认没有其他并发读写者后,才能清掉字段引用。
p.ticker = nil
}()
for {
select {
case
这里最关键的不是 defer,而是 ctx.Done() 和 ticker.C 同时出现在 select 中。Stop 负责停止后续 tick,context 负责让业务循环返回,两者职责分开,旧引用才有机会在返回后被统一清理。

Go 1.23 之后,GC 变化了但引用语义没有变化
Go 官方文档说明,Go 1.23 之后,垃圾回收器可以回收“已经没有引用”的 ticker,即使它还没有显式调用 Stop。这解决的是 ticker 在无人使用时的回收问题,不是把仍被业务对象、闭包或 goroutine 持有的引用强行断开。
因此不要把“Go 1.23 可以回收未停止 ticker”理解成“Stop 后旧指针应该自动消失”。更准确的判断方式是:
- 变量仍然指向
*time.Ticker:这是调用方仍持有引用,GC 不会因为Stop而修改它。 - 接收循环仍然持有
ticker.C:通道不会因Stop自动关闭,接收方需要额外退出信号。 - 所有业务引用都已退出且变量不可达:此时才属于 GC 可以自行处理的范围。
如果代码只需要一个不能主动停止的简单 tick 通道,time.Tick 在 Go 1.23 以后不再因为“无法 Stop”而天然阻止 GC;但只要业务有明确的关闭、取消、重启或资源回收阶段,我仍建议使用 NewTicker,因为它让生命周期拥有者清楚地表达停止动作。
清理旧引用时要先处理并发访问
把字段直接改成 nil 不是万能修复。如果另一个 goroutine 可能同时读取这个字段,清空动作本身又会制造数据竞争,甚至出现空指针。清理顺序应该围绕所有权来定:
- 先发送取消信号或调用上层
cancel(),让接收循环有机会返回。 - 等待接收 goroutine 完成,确认它不再读取 ticker。
- 由唯一拥有者调用
Stop,并在同步边界内清空字段。 - 下次启动时创建新的 ticker,不要复用已经交给旧循环的 channel。
如果服务会重复启动和停止,可以把取消函数、完成通知和 ticker 放进同一个生命周期对象中;不要让外部调用方同时持有 ticker 指针和停止权限。这样排查“旧引用还在”时,问题会从“GC 为什么没清理”变成更容易回答的“谁还拥有这个对象”。
用这张清单判断问题到底在哪一层
| 观察到的现象 | 优先检查 | 正确处理 |
|---|---|---|
| Stop 后指针值仍可打印 | 调用方是否还保存字段或局部变量 | 区分停止动作与引用清理,必要时在安全边界置 nil |
| goroutine 数量不下降 | 接收循环是否只监听 ticker.C | 加入 context.Done() 或独立 done channel,并等待退出 |
尝试关闭 ticker.C | 是否把只读接收通道当成业务关闭通道 | 不要关闭 ticker.C,用退出信号结束消费者 |
| Go 1.23 后仍感觉对象没回收 | 是否还有结构体、闭包或 goroutine 引用 | 先消除业务引用,再把 GC 回收当作运行时实现细节 |
常见问题
Stop 会不会让 ticker.C 立即返回零值或关闭状态?
不会。Stop 不关闭 C,接收方不能靠通道关闭来判断 ticker 停止,应使用 context 或独立 done channel。
把 ticker 指针设为 nil 就能让 goroutine 退出吗?
不能。置 nil 只影响当前引用,不能唤醒已经阻塞在 上的 goroutine;必须提供退出信号并等待循环返回。
Go 1.23 之后还需要调用 Stop 吗?
如果只是为了帮助 GC,未被引用的 ticker 已可被回收;但如果业务需要立刻停止发送、结束循环或表达资源关闭语义,仍然应该调用 Stop。
可以在多个 goroutine 中同时调用 Stop 吗?
不要把 Stop 权限随意分散。更稳妥的做法是让一个生命周期拥有者负责取消、等待接收循环结束和 Stop,避免清理逻辑互相竞争。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
397 收藏
-
459 收藏
-
488 收藏
-
487 收藏
-
124 收藏
-
447 收藏
-
263 收藏
-
483 收藏
-
329 收藏
-
294 收藏
-
125 收藏
-
239 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习