如何用Go编写毫秒级定时器?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1094个文字,预计阅读时间需要5分钟。
定时执行功能最常用、最轻量级的实现方式是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配合donechannel - 不要在回调里做耗时操作,否则会阻塞该 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())。
本文共计1094个文字,预计阅读时间需要5分钟。
定时执行功能最常用、最轻量级的实现方式是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配合donechannel - 不要在回调里做耗时操作,否则会阻塞该 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())。

