154 lines
4.1 KiB
Markdown
154 lines
4.1 KiB
Markdown
---
|
||
tags: [go, golang, 定时器, Timer, Ticker, 并发]
|
||
create time: 2026-06-07 15:05
|
||
---
|
||
|
||
# 定时器
|
||
|
||
## 概述
|
||
|
||
Go 标准库提供了 `time.Timer`(一次性定时器)和 `time.Ticker`(周期性定时器),它们是构建超时控制、心跳检测、定时任务等并发模式的基础工具。
|
||
|
||
## 正文
|
||
|
||
### Timer — 一次性定时器
|
||
|
||
> [!question] 💭 思考
|
||
> 如何在一个 goroutine 中等待指定时间后执行某个操作?用 `time.Sleep`?但如果需要提前取消呢?
|
||
|
||
```go
|
||
timer := time.NewTimer(2 * time.Second) // 2秒后触发
|
||
<-timer.C // 阻塞直到触发
|
||
fmt.Println("timeout!")
|
||
```
|
||
|
||
`Timer` 的核心是 `C <-chan Time`——一个在触发时刻写入当前时间的 channel。
|
||
|
||
#### 停止与重置
|
||
|
||
```go
|
||
timer := time.NewTimer(5 * time.Second)
|
||
|
||
// 尝试在触发前停止
|
||
if !timer.Stop() {
|
||
<-timer.C // 如果已经触发,需要从 C 中读出值
|
||
}
|
||
|
||
// 重置定时器
|
||
timer.Reset(3 * time.Second) // 3秒后重新触发
|
||
```
|
||
|
||
> [!warning] ⚠️ Stop/Reset 的返回值陷阱
|
||
> - `Stop()` 返回 `true`:定时器还未触发,成功停止
|
||
> - `Stop()` 返回 `false`:定时器已触发,**C 通道中可能已有值**
|
||
>
|
||
> 当 `Stop()` 返回 false 时,如果后续代码仍需读取 timer.C,必须先消费掉那个值,否则会导致泄漏。
|
||
|
||
> [!tip] 💡 安全使用模式
|
||
> ```go
|
||
> timer := time.NewTimer(d)
|
||
> select {
|
||
> case <-timer.C:
|
||
> // 超时处理
|
||
> case <-someChannel:
|
||
> timer.Stop()
|
||
> if !timer.Stop() {
|
||
> <-timer.C // 清理残留值
|
||
> }
|
||
> }
|
||
> ```
|
||
|
||
#### AfterFunc — 延迟执行回调
|
||
|
||
```go
|
||
// 1秒后执行 f,返回 Timer 以便取消
|
||
timer := time.AfterFunc(1*time.Second, func() {
|
||
fmt.Println("after 1 second")
|
||
})
|
||
|
||
// 如果需要取消
|
||
timer.Stop()
|
||
```
|
||
|
||
#### After — 便捷超时
|
||
|
||
```go
|
||
select {
|
||
case result := <-doWork():
|
||
fmt.Println("完成:", result)
|
||
case <-time.After(5 * time.Second):
|
||
fmt.Println("超时!")
|
||
}
|
||
```
|
||
|
||
> [!note] 📝 After 的实现
|
||
> `time.After(d)` 等价于 `time.NewTimer(d).C`。它创建了一个内部 Timer,但无法提前释放——所以长生命周期场景推荐手动管理 Timer。
|
||
|
||
### Ticker — 周期性定时器
|
||
|
||
Ticker 每隔固定间隔向通道发送信号,适用于轮询、心跳等场景:
|
||
|
||
```go
|
||
ticker := time.NewTicker(1 * time.Second)
|
||
|
||
// 务必在退出时停止
|
||
defer ticker.Stop()
|
||
|
||
for tick := range ticker.C {
|
||
fmt.Println("tick at", tick)
|
||
// 如果只需要执行一次
|
||
break
|
||
}
|
||
```
|
||
|
||
> [!warning] ⚠️ Ticker 的资源泄漏
|
||
> - `ticker.Stop()` **只停止发送新信号,不会关闭 ticker.C 通道**
|
||
> - 必须通过其他方式(如 context.Done / break)退出循环,否则 goroutine 泄漏
|
||
|
||
```go
|
||
func periodicWork(ctx context.Context, interval time.Duration) {
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop() // ✅ 确保释放资源
|
||
|
||
for {
|
||
select {
|
||
case <-ticker.C:
|
||
doWork()
|
||
case <-ctx.Done():
|
||
return // ✅ 正常退出
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 常见模式对比
|
||
|
||
| 需求 | 推荐方案 | 说明 |
|
||
|------|---------|------|
|
||
| 等待 N 秒后执行一次 | `time.After` | 简洁,适合短生命周期 |
|
||
| 可取消的延迟执行 | `time.AfterFunc` | 返回 Timer 可调用 Stop |
|
||
| 周期性执行 + 可控退出 | `Ticker + context` | 灵活,资源可控 |
|
||
| 精确单次触发 | `time.NewTimer` | 可 Stop/Reset,资源可控 |
|
||
|
||
### 精度问题
|
||
|
||
> [!info] ℹ️ 补充知识
|
||
> - Timer/Ticker 的精度依赖于操作系统调度,不是严格精确的
|
||
> - 在高精度场景中,应设置缓冲避免错过 tick:`time.NewTicker(d)` 如果消费跟不上会产生积压
|
||
> - Ticker 消费不及时时,多余的 tick 会被丢弃(通道满时阻塞发送方)
|
||
|
||
```go
|
||
// ❌ 危险:无缓冲,消费慢时会阻塞
|
||
ticker := time.NewTicker(1 * time.Millisecond)
|
||
|
||
// ✅ 推荐:有缓冲,允许短暂积压
|
||
ticker := time.NewTicker(1 * time.Millisecond)
|
||
// 配合 context 控制生命周期
|
||
```
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/GolangStar/Go语言进阶/Select]]
|
||
- [[hzh/GolangStar/Go语言进阶/Context]]
|
||
- [[hzh/GolangStar/Go语言进阶/Goroutine]]
|