vault backup: 2026-06-07 12:14:39

This commit is contained in:
2026-06-07 12:14:39 +08:00
parent 1a72bc4d82
commit 7ce9b83218
61 changed files with 8409 additions and 14429 deletions
+131 -231
View File
@@ -1,253 +1,153 @@
---
tags:
- Go
- golang
- go进阶
- 定时器
- 并发
tags: [go, golang, 定时器, Timer, Ticker, 并发]
create time: 2026-06-07 15:05
---
# 定时器
在我们项目中,常常会有这样的场景,比如到了未来某一时刻,需要某个逻辑或者某个任务执行一次,或者是周期性的的执行多次,有点类似定时任务。这种场景就需要用到定时器,Go语言中也内置了定时器的实现,`timer`和`ticker`。
## Timer
`Timer`是一种一次性时间定时器,即在未来某个时刻,触发的事件只会执行一次。
## 概述
### Timer的结构定义
```go
type Timer struct {
C <-chan Time
r runtimeTimer
}
```
`Timer`结构里有一个`Time`类型的管道`C`,主要用于事件通知。在未到达设定时间的时候,管道内没有数据写入,一直处于阻塞状态,到达设定时间后,会向管道内写入一个系统时间,触发事件。
Go 标准库提供了 `time.Timer`(一次性定时器)和 `time.Ticker`(周期性定时器),它们是构建超时控制、心跳检测、定时任务等并发模式的基础工具。
### 创建Timer
```go
func NewTimer(d Duration) *Timer
```
使用示例:
```go
package main
## 正文
import (
"fmt"
"time"
)
### Timer — 一次性定时器
func main() {
timer := time.NewTimer(2 * time.Second) //设置超时时间2s
<-timer.C
fmt.Println("after 2s Time out!")
}
```
运行结果:
```
after 2s Time out!
```
程序在`2s`后打印`"after 2s Time out!"`,因为创建了一个定时器`timer`,设置了超时时间为`2s`,执行`<-timer.C`会一直阻塞,直到`2s`后,程序继续执行。
### 停止Timer
```go
func (t *Timer) Stop() bool
```
返回值:
- `true`:执行`stop()`时`timer`还没有到达超时时间,即超时时间内停止了`timer`
- `false`:执行`stop()`时`timer`到达了超时时间,过了超时时间才停止`timer`
使用示例:
```go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(2 * time.Second) //设置超时时间2s
res := timer.Stop()
fmt.Println(res)
}
```
运行结果:
```
true
```
### 重置Timer
> [!question] 💭 思考
> 如何在一个 goroutine 中等待指定时间后执行某个操作?用 `time.Sleep`?但如果需要提前取消呢?
```go
func (t *Timer) Reset(d Duration) bool
timer := time.NewTimer(2 * time.Second) // 2秒后触发
<-timer.C // 阻塞直到触发
fmt.Println("timeout!")
```
对于已经过期或者是已经停止的`timer`,可以通过重置方法激活使其继续生效。
使用示例:
```go
package main
`Timer` 的核心是 `C <-chan Time`——一个在触发时刻写入当前时间的 channel。
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(time.Second * 2)
<-timer.C
fmt.Println("time out1")
res1 := timer.Stop()
fmt.Printf("res1 is %t\n", res1)
timer.Reset(time.Second * 3)
res2 := timer.Stop()
fmt.Printf("res2 is %t\n", res2)
}
```
运行结果:
```
time out1
res1 is false
res2 is true
```
程序2s之后打印"time out1",此时`timer`已经过期了,所以`res1`的值为`false`,接下来执行`timer.Reset(time.Second * 3)`又使`timer`生效了,并且重设超时时间为3s,但是紧接着执行了`timer.Stop()`,还未到超时时间,所以`res2`的值为`true`。
### time.AfterFunc
方法定义:
```go
func AfterFunc(d Duration, f func()) *Timer
```
`time.AfterFunc`参数为超时时间`d`和一个具体的函数`f`,返回一个`Timer`的指针,作用在创建出`timer`之后,在当前`goroutine`,等待一段时间`d`之后,将执行`f`。
使用示例:
```go
package main
import (
"fmt"
"time"
)
func main() {
duration := time.Duration(1) * time.Second
f := func() {
fmt.Println("f has been called after 1s by time.AfterFunc")
}
timer := time.AfterFunc(duration, f)
defer timer.Stop()
time.Sleep(2 * time.Second)
}
```
运行结果:
```
f has been called after 1s by time.AfterFunc
```
1s之后打印语句。
### time.After
方法定义:
```go
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
```
根据函数定义可以看到,`after`函数会返回`timer`里的管道,并且这个管道会在经过时段`d`之后写入数据,调用这个函数,就相当于实现了定时器。一般`time.After`会配合`select`一起使用,使用示例如下:
#### 停止与重置
```go
package main
timer := time.NewTimer(5 * time.Second)
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(time.Second * 3)
ch <- "test"
}()
select {
case val := <-ch:
fmt.Printf("val is %s\n", val)
case <-time.After(time.Second * 2):
fmt.Println("timeout!!!")
}
}
```
运行结果:
```
timeout!!!
```
程序创建了一个管道`ch`,并且在主`goroutine`用`select`监听两个管道,一个是刚刚创建的`ch`,一个是`time.After`函数返回的管道`c`,`ch`管道3s之后才会有数据写入,而`time.After`函数是2s超时,所以2s后就会有数据写入,这样`select`会先收到管道`c`里的数据,执行`timeout`退出。
## Ticker
### Ticker创建
方法定义如下:
```go
func NewTicker(d Duration) *Ticker
```
`NewTicker`用于返回一个`Ticker`对象。
### Ticker对象定义
```go
type Ticker struct {
C <-chan Time // The channel on which the ticks are delivered.
r runtimeTimer
}
```
`Ticker`对象的字段和`Timer`是一样的,也包含一个通道字段,并会每隔时间段`d`就向该通道发送当时的时间,根据这个管道消息来触发事件,但是`ticker`只要定义完成,就从当前时间开始计时,每隔固定时间都会触发,只有关闭`Ticker`对象才不会继续发送时间消息。
使用示例:
```go
package main
import (
"fmt"
"time"
)
func Watch() chan struct{} {
ticker := time.NewTicker(1 * time.Second)
ch := make(chan struct{})
go func(ticker *time.Ticker) {
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("watch!!!")
case <-ch:
fmt.Println("Ticker Stop!!!")
return
}
}
}(ticker)
return ch
// 尝试在触发前停止
if !timer.Stop() {
<-timer.C // 如果已经触发,需要从 C 中读出值
}
func main() {
ch := Watch()
time.Sleep(5 * time.Second)
ch <- struct{}{}
close(ch)
// 重置定时器
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("超时!")
}
```
运行结果:
```
watch!!!
watch!!!
watch!!!
watch!!!
watch!!!
Ticker Stop!!!
```
`Watch`函数里创建一个`ticker`,将它传递到子`goroutine`函数,每隔1s打印"watch!!!",主函数创建一个管道`ch`,通过`ch`来控制`go func()`函数的退出,在5s之后主函数发送一个信号到`ch`,`watch`函数`select`收到`ch`信号,将`return`,在`return`之前将执行`defer ticker.Stop()`语句关闭`ticker`。在这5s之间,`select`将每个1s收到`ticker.C`管道里的消息,打印"watch!!!"。
> 注意:调用`ticker.Stop()`只会停止`ticker`,但并不会关闭`ticker.C`这个管道,所以我们需要用这个`channel`来控制`watch`函数中的`goroutine`能够退出。
> [!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]]