如何用Go编写毫秒级定时器?

2026-04-24 18:542阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1094个文字,预计阅读时间需要5分钟。

如何用Go编写毫秒级定时器?

定时执行功能最常用、最轻量级的实现方式是time.AfterFunc。它不会阻塞主线程,适合做“延迟执行一次的操作(如延迟300ms发送埋点、防抖触发等)。

注意:传入的毫秒数要转成 time.Duration,直接写 300 * time.Millisecond,别用 time.Duration(300)——后者单位是纳秒,实际只等 300 纳秒,几乎立刻触发。

常见错误现象:AfterFunc 回调没执行?大概率是调用后程序提前退出了(比如 main 函数结束),Go 的 main 退出会终止所有 goroutine。加个 select{}time.Sleep 拖住主 goroutine 即可。

  • 示例:time.AfterFunc(500*time.Millisecond, func() { fmt.Println("500ms 后执行") })
  • 如果需要取消,改用 time.After + select 配合 done channel
  • 不要在回调里做耗时操作,否则会阻塞该 timer 所在的系统 goroutine(虽然不影响其他 timer,但可能拖慢同一批次的其他定时器)

time.Ticker 实现周期性毫秒触发

time.Ticker 是标准库中唯一支持稳定周期调度的类型,适合每 20ms 采样一次传感器、每 100ms 心跳上报这类需求。

关键点:最小间隔受限于系统调度精度和 Go runtime 的 timer 实现。Linux 下通常能稳定到 1–2ms;Windows 可能偏差更大(默认系统时钟粒度 15.6ms),可通过 timeBeginPeriod(1) 调整(需调用 winapi,非纯 Go)。

  • 创建:ticker := time.NewTicker(20 * time.Millisecond)
  • 必须手动 ticker.Stop(),否则 goroutine 和 timer 会泄漏(GC 不回收活跃 timer)
  • 读取通道时别用 for range ticker.C——万一 ticker 停了,range 会 panic;推荐 for { select { case 配合退出条件
  • 如果处理逻辑耗时超过周期(比如每 10ms 触发,但每次处理要 15ms),ticker.C 缓存会堆积,导致“追赶式”连发。此时应改用 time.After 在每次处理完再算下一次时间

避免用 time.Sleep 做毫秒级轮询

time.Sleep 表面看能“每隔 X ms 做件事”,但它是阻塞当前 goroutine 的,且误差累积严重:每次 Sleep 返回后还要执行业务逻辑,再进入下次 Sleep,实际间隔 = Sleep 时间 + 业务耗时。

更糟的是,Go 的 Sleep 底层依赖系统调用,在高负载或虚拟机环境下,10ms Sleep 经常变成 12–18ms,抖动不可控。

  • 错误写法:for { doWork(); time.Sleep(10 * time.Millisecond) }
  • 正确替代:用 time.Ticker(固定周期)或 time.After + 循环重置(动态间隔)
  • 若真要 sleep 控制节奏,至少用 time.Sleep(time.Until(nextTime)),把下次触发时间锚定死,减少漂移

精度不够时,得接受“Go 本身不保证亚毫秒级调度”

Go 的 timer 基于 epoll/kqueue/IOCP 构建,不是实时系统。即使代码写成 1ms,runtime 内部也会合并相近的 timer 到同一个底层事件源,实际唤醒可能有 ±0.1–0.5ms 偏差。这不是 bug,是设计取舍。

如果你的应用要求严格等间隔(如音频采样、工业控制),Go 不是首选语言;若只是 UI 刷新、网络重试、日志刷盘这类场景,time.Ticker 配合合理间隔(≥5ms)已足够可靠。

容易被忽略的一点:GC STW 期间,所有 timer 都暂停。如果设置了 2ms 的 ticker,而某次 GC 持续了 3ms,那这次 tick 就会丢失——没有“补发”机制。对可靠性敏感的场景,必须在业务逻辑里检测时间跳变(比如记录上一次触发时间戳,对比 time.Now())。

标签:Go

本文共计1094个文字,预计阅读时间需要5分钟。

如何用Go编写毫秒级定时器?

定时执行功能最常用、最轻量级的实现方式是time.AfterFunc。它不会阻塞主线程,适合做“延迟执行一次的操作(如延迟300ms发送埋点、防抖触发等)。

注意:传入的毫秒数要转成 time.Duration,直接写 300 * time.Millisecond,别用 time.Duration(300)——后者单位是纳秒,实际只等 300 纳秒,几乎立刻触发。

常见错误现象:AfterFunc 回调没执行?大概率是调用后程序提前退出了(比如 main 函数结束),Go 的 main 退出会终止所有 goroutine。加个 select{}time.Sleep 拖住主 goroutine 即可。

  • 示例:time.AfterFunc(500*time.Millisecond, func() { fmt.Println("500ms 后执行") })
  • 如果需要取消,改用 time.After + select 配合 done channel
  • 不要在回调里做耗时操作,否则会阻塞该 timer 所在的系统 goroutine(虽然不影响其他 timer,但可能拖慢同一批次的其他定时器)

time.Ticker 实现周期性毫秒触发

time.Ticker 是标准库中唯一支持稳定周期调度的类型,适合每 20ms 采样一次传感器、每 100ms 心跳上报这类需求。

关键点:最小间隔受限于系统调度精度和 Go runtime 的 timer 实现。Linux 下通常能稳定到 1–2ms;Windows 可能偏差更大(默认系统时钟粒度 15.6ms),可通过 timeBeginPeriod(1) 调整(需调用 winapi,非纯 Go)。

  • 创建:ticker := time.NewTicker(20 * time.Millisecond)
  • 必须手动 ticker.Stop(),否则 goroutine 和 timer 会泄漏(GC 不回收活跃 timer)
  • 读取通道时别用 for range ticker.C——万一 ticker 停了,range 会 panic;推荐 for { select { case 配合退出条件
  • 如果处理逻辑耗时超过周期(比如每 10ms 触发,但每次处理要 15ms),ticker.C 缓存会堆积,导致“追赶式”连发。此时应改用 time.After 在每次处理完再算下一次时间

避免用 time.Sleep 做毫秒级轮询

time.Sleep 表面看能“每隔 X ms 做件事”,但它是阻塞当前 goroutine 的,且误差累积严重:每次 Sleep 返回后还要执行业务逻辑,再进入下次 Sleep,实际间隔 = Sleep 时间 + 业务耗时。

更糟的是,Go 的 Sleep 底层依赖系统调用,在高负载或虚拟机环境下,10ms Sleep 经常变成 12–18ms,抖动不可控。

  • 错误写法:for { doWork(); time.Sleep(10 * time.Millisecond) }
  • 正确替代:用 time.Ticker(固定周期)或 time.After + 循环重置(动态间隔)
  • 若真要 sleep 控制节奏,至少用 time.Sleep(time.Until(nextTime)),把下次触发时间锚定死,减少漂移

精度不够时,得接受“Go 本身不保证亚毫秒级调度”

Go 的 timer 基于 epoll/kqueue/IOCP 构建,不是实时系统。即使代码写成 1ms,runtime 内部也会合并相近的 timer 到同一个底层事件源,实际唤醒可能有 ±0.1–0.5ms 偏差。这不是 bug,是设计取舍。

如果你的应用要求严格等间隔(如音频采样、工业控制),Go 不是首选语言;若只是 UI 刷新、网络重试、日志刷盘这类场景,time.Ticker 配合合理间隔(≥5ms)已足够可靠。

容易被忽略的一点:GC STW 期间,所有 timer 都暂停。如果设置了 2ms 的 ticker,而某次 GC 持续了 3ms,那这次 tick 就会丢失——没有“补发”机制。对可靠性敏感的场景,必须在业务逻辑里检测时间跳变(比如记录上一次触发时间戳,对比 time.Now())。

标签:Go