目录
[一、什么是 Go 语言中的定时器?](#一、什么是 Go 语言中的定时器?)
[Go 语言中的实现](#Go 语言中的实现)
[三、Go 定时器的内部实现原理](#三、Go 定时器的内部实现原理)
[四、time.Timer ------ 一次性定时器](#四、time.Timer —— 一次性定时器)
[1. 基本用法](#1. 基本用法)
[2. 另一种方式:time.After](#2. 另一种方式:time.After)
[五、time.Ticker ------ 周期性定时器](#五、time.Ticker —— 周期性定时器)
一、什么是 Go 语言中的定时器?
基本概念
定时器(Timer):就像一次性闹钟,设置后在指定时间触发一次事件,适合延迟执行或超时控制。
计数器(Ticker):像每小时响一次的钟,定期重复触发事件,适合周期性任务。
Go 语言中的实现
Go 的 time 包提供了两个核心工具:
time.Timer:用于一次性定时任务。
time.Ticker:用于周期性任务。
二、定时器的产生背景
最原始的问题:程序需要"等待一段时间"
在计算机早期(单线程执行时代),程序的执行是连续 的。
但是很多场景需要"延时执行"或"定时触发":
-
例如:
-
网络请求超时控制(5 秒无响应就退出)
-
每隔一段时间采集一次数据
-
定时清理缓存或 session
-
周期性健康检查(health check)
-
执行计划任务(类似 cron job)
-
这些任务都需要一种机制:"在未来的某个时间点执行某个动作"。
三、Go 定时器的内部实现原理
Go runtime 的定时器系统是由调度器(scheduler)管理的。
主要基于 最小堆(min-heap)结构。
思想:
所有活跃的
Timer、Ticker都存储在一个堆中;堆顶是下一个即将触发的定时器;
runtime 中有一个专门的 goroutine(叫 timerproc)循环运行:
检查堆顶的到期时间;
睡眠到该时间;
时间一到,就唤醒并执行对应的回调或发送信号。
伪代码示意:
Go
for {
t := heap.Top() // 最近的定时器
sleepUntil(t.when)
trigger(t) // 执行任务
}
这种机制的好处:
所有定时任务集中管理;
效率极高(对堆的操作,O(log n) 插入、O(1) 取最早任务);
四、time.Timer ------ 一次性定时器
1. 基本用法
Go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second)
fmt.Println("等待中...")
<-timer.C // 阻塞,直到2秒后收到信号
fmt.Println("2秒到了!")
}
解释:
time.NewTimer(d)创建一个定时器;
timer.C是一个只读 channel;当时间到达后,系统会向
timer.C发送一个时间值;
<-timer.C会阻塞直到时间到。
2. 另一种方式:time.After
time.After 是一个简化版本,会在指定时间后返回一个 channel:
Go
<-time.After(3 * time.Second) fmt.Println("3秒到了!")
这其实相当于:
Go
timer := time.NewTimer(3 * time.Second) <-timer.C
适合简单一次性延迟的场景。
3.取消定时器:Stop()
如果不再需要一个定时器(例如提前退出、释放资源),应该调用:
Go
timer := time.NewTimer(5 * time.Second)
go func() {
<-timer.C
fmt.Println("定时任务执行")
}()
time.Sleep(2 * time.Second)
if timer.Stop() {
fmt.Println("定时器被取消")
}
注意:如果定时器已经触发(channel 已经有值),
Stop()会返回false。
4.重置定时器:Reset()
如果想复用一个定时器:
Go
timer := time.NewTimer(2 * time.Second)
<-timer.C // 第一次触发
timer.Reset(1 * time.Second) // 重新开始计时
<-timer.C
fmt.Println("又过了1秒")
五、time.Ticker ------ 周期性定时器
1.开启定时器。
Go
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for t := range ticker.C {
fmt.Println("时间到了:", t)
}
每隔 1 秒,ticker.C 就会收到一个时间值。
2.停止定时器
必须手动调用:
Go
ticker.Stop()
否则 goroutine 不会结束,可能导致内存泄漏。
六、定时器案例
1.用timer模拟闹钟贪睡
Go
package main
import (
"fmt"
"time"
)
func main() {
// 闹钟,自动重试任务
// NewTimer 创建一个 Timer,它会在最少过去时间段 d 后到期,向其自身的 C 字段发送当时的时间
timer1 := time.NewTimer(5 * time.Second)
fmt.Println("开始时间:", time.Now().Format("2006-01-02 15:04:05"))
go func(t *time.Timer) {
times := 0
for {
<-t.C
fmt.Println("timer", time.Now().Format("2006-01-02 15:04:05"))
// 从t.C中获取数据,此时time.Timer定时器结束。如果想再次调用定时器,只能通过调用 Reset() 函数来执行
// Reset 使 t 重新开始计时,(本方法返回后再)等待时间段 d 过去后到期。
// 如果调用时 t 还在等待中会返回真;如果 t已经到期或者被停止了会返回假。
times++
// 调用 reset 重发数据到chan C
fmt.Println("调用 reset 重新设置一次timer定时器,并将时间修改为2秒")
t.Reset(2 * time.Second)
if times > 3 {
fmt.Println("调用 stop 停止定时器")
t.Stop()
}
}
}(timer1)
time.Sleep(30 * time.Second)
fmt.Println("结束时间:", time.Now().Format("2006-01-02 15:04:05"))
fmt.Println("ok")
}
2.用ticker模拟心跳检测
Go
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop() // 函数结束时关闭 ticker
done := make(chan bool)
// 模拟外部协程:10秒后关闭心跳
go func() {
time.Sleep(10 * time.Second)
done <- true
}()
for {
select {
case t := <-ticker.C:
fmt.Println("发送心跳包:", t)
case <-done:
fmt.Println("停止发送心跳包")
return
}
}
}
我们可以通过以上案例看到,timer定时器和reset函数组合,也可以达到ticker的效果。
七、总结
对比
Timer:一次性触发,适合延迟任务或超时控制。
Ticker:周期性触发,适合重复任务。
选择场景
Timer:如 HTTP 请求超时、延迟执行任务。
Ticker:如定时刷新数据、心跳检测。
注意事项
Ticker 如果不停止,会持续运行,可能导致资源泄露。始终记得在不需要时调用 Stop()。
Timer 触发后会自动失效,无需担心资源问题。