当前位置:首页 > 文章列表 > Golang > Go问答 > Go Ticker 为什么停止后仍然被旧引用持有

Go Ticker 为什么停止后仍然被旧引用持有

来源:17golang原创 2026-10-26 00:19:39 0浏览 收藏

我第一次遇到这个问题时,表面现象很像 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 的运行语义,引用是否还在则是调用方的所有权问题。

Go Ticker 的调用方引用、ticker.C、Stop、不关闭通道与 GC 可回收边界说明图
图1:边界说明图,展示 Stop 停止发送与旧引用仍然可达之间的关系,不是运行截图。

最容易漏掉的是被阻塞的接收 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 Ticker 接收 goroutine 同时监听 ticker.C 与 context.Done 并在 Stop 后返回清理的结构说明图
图2:结构说明图,展示可退出接收循环的信号边界和清理责任,不是并发运行证据。

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 可能同时读取这个字段,清空动作本身又会制造数据竞争,甚至出现空指针。清理顺序应该围绕所有权来定:

  1. 先发送取消信号或调用上层 cancel(),让接收循环有机会返回。
  2. 等待接收 goroutine 完成,确认它不再读取 ticker。
  3. 由唯一拥有者调用 Stop,并在同步边界内清空字段。
  4. 下次启动时创建新的 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,避免清理逻辑互相竞争。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go Timer Stop 为什么仍然收到一次事件Go Timer Stop 为什么仍然收到一次事件
上一篇
Go Timer Stop 为什么仍然收到一次事件
Go atomic.Int32 怎样用 CompareAndSwap 管理有限状态
下一篇
Go atomic.Int32 怎样用 CompareAndSwap 管理有限状态
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    693次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    705次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    770次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    732次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    565次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码