一、time 包的定时器
在日常开发中,我们可能会遇到需要延迟执行或周期性地执行一些任务。这个时候就需要用到 Go 语言中的定时器。
在 Go 语言中,定时器类型有两种:time.Timer 一次性定时器 和 time.Ticker 周期性定时器。
1.1 time.Timer 一次性定时器
time.Timer
是一个一次性的定时器,用于在未来的某一时刻执行一次操作。
Tips:time.Timer 源码位于 src/time/sleep.go 中
1、Timer 一次性定时器的基本使用 —— 创建定时器函数
创建 Timer 定时器的方式有两种:
NewTimer(d Duration) *Timer
:该函数接受一个 time.Duration 类型的参数 d(时间间隔),表示定时器在过期之前等待的时间;NewTimer 返回一个新的 Timer 定时器,这个定时器在其内部维护一个通道 C,该通道在定时器被触发时会接收当前的时间值;
AfterFunc(d Duration, f func()) *Timer
:接受一个指定的时间间隔 d 和 回调函数 f;该函数返回一个新的 Timer 定时器,在定时器到期时直接调用 f,而不是通过通道 C 发送信号。调用 Timer 的 Stop 方法可以停止定时器 和 取消调用 f;
下面的代码展示了如何使用 NewTimer 和 AfterFunc 来创建定时器以及定时器的基本用法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package main
import (
"fmt"
"time"
)
func main() {
// 使用 NewTimer 创建一个定时器
timer := time.NewTimer(time.Second)
go func() {
select {
case <-timer.C: // 在一个新的 goroutine 中监听它的 C 属性以等待定时器触发
fmt.Println("timer 定时器触发啦!")
}
}()
// 使用 AfterFunc 创建另一个定时器,通过指定一个 回调函数 来处理定时器到期事件
time.AfterFunc(time.Second, func() {
fmt.Println("timer2 定时器触发啦!")
})
// 主goroutine等待两秒,确保看到定时器触发的输出
time.Sleep(time.Second * 2)
}
|
2、Reset 方法重置 Timer 定时器使用详解
Reset(d Duration) bool
:方法用于重置 Timer 定时器的过期时间,也可以理解为重新激活定时器。它接受一个 time.Duration 类型的参数 d,表示定时器在过期之前等待的时间。
除此之外,该方法还返回一个 bool 值:
- 如果定时器处于活动的状态,返回 true;
- 如果定时器已经过期或被停止了,返回 false(false 并不意味着激活定时器失败,只是标识定时器的当前状态);
Reset 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(5 * time.Second) // 创建了一个定时器,设置为 5 秒后到期
// 调用 Reset 方法立即将其重置为 1 秒后到期;
// 因为此时定时器仍处于激活状态(即还未到期),所以 Reset 方法返回 true
b := timer.Reset(1 * time.Second)
fmt.Println(b) // true
second := time.Now().Second()
select {
case t := <-timer.C: // select 语句等待定时器到期,并打印出实际经过的秒数(约等于 1 秒)
fmt.Println(t.Second() - second) // 1s
}
// 第二次调用 Reset 重置为 2 秒后到期。由于定时器在这次重置时已经到期(处于过期状态),Reset 方法返回 false。
b = timer.Reset(2 * time.Second)
fmt.Println(b) // false
second = time.Now().Second()
select {
case t := <-timer.C: // 再次使用 select 语句等待定时器到期,并打印出这次经过的秒数(约等于 2 秒)
fmt.Println(t.Second() - second) // 2s
}
// 主goroutine等待两秒,确保看到定时器触发的输出
time.Sleep(time.Second * 3)
}
|
3、Stop 方法停止 Timer 定时器使用详解
Stop() bool
:方法用于停止定时器。如果定时器停止成功,返回 true,如果定时器已经过期或被停止,则返回 false。切记:Stop 操作不会关闭通道 C。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(3 * time.Second)
// 停止定时器,在定时器触发之前停止它,因此返回 true
stop := timer.Stop()
fmt.Println(stop) // true
stop = timer.Stop()
// 第二次停止定时器,此时定时器已经被停止了,返回 false
fmt.Println(stop) // false
}
|
1.2 time.Ticker 周期性定时器
time.Tciker
是一个周期性的定时器,用于在固定的时间间隔重复执行任务。它在每个间隔时间到来时,向其通道(Channel)发送当前时间。
Tips:time.Ticker 源码位于 src/time/tick.go 中
1、time.Ticker 的基本使用
使用 NewTicker 函数来创建一个新的 Ticker 对象,该函数接受一个 time.Duration 类型的参数 d(时间间隔)。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
package main
import (
"context"
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second) // 创建了一个每秒触发的定时器
defer ticker.Stop() // 确保函数周期结束后清理定时器
// 创建一个在 5 秒后超时的上下文
timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*5)
defer cancelFunc() // 退出前清理上下文
// 在一个新的 goroutine 中,select 语句用于监听两个通道:
// 定时器的通道 (ticker.C) 和 超时上下文的完成通道 (timeout.Done())。
// 当定时器每秒触发时,会打印出消息。
// 当上下文超时(即 5 秒过后),打印出超时信息,并返回从而结束该 goroutine。
go func() {
for {
select {
case <-timeout.Done():
fmt.Println("timeout done")
return
case <-ticker.C:
fmt.Println("定时器触发啦!")
}
}
}()
// 主goroutine等待 7 秒,确保看到定时器触发的输出
time.Sleep(time.Second * 7)
}
|
除了使用 select 语句监听 ticker.C 以外,还可以使用 for range 的形式进行监听:
1
2
3
|
for range ticker.C {
// ToDo: worker
}
|
需要注意的是,即使通过 Stop 方法停止 Ticker 定时器,其 C 通道不会被关闭。这意味着无论是通过 for select 还是 for range 去监听 ticker.C,都需要使用其它机制来退出循环,例如使用 context 上下文。
2、Reset 方法重置 Ticker 定时器使用详解
Reset(d Duration)
方法用于停止当前计时器并将其周期重置为指定的时间。下一个时间刻度将在新周期结束后生效。它接受一个 time.Duration 类型的参数 d,表示新的周期。该参数必须大于零;否则 Reset 方法内部将会 panic。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package main
import (
"time"
)
func main() {
ticker := time.NewTicker(5 * time.Second) // 创建了一个每 5 秒触发一次的定时器 time.Ticker
defer ticker.Stop() // 确保函数周期结束后清理定时器
ticker.Reset(1 * time.Second) // 使用 Reset 方法重置定时器的触发间隔,5 秒变成 1 秒
second := time.Now().Second()
for t := range ticker.C { // 最后通过一次循环,打印定时器的周期,预期结果为 1 秒
fmt.Printf("周期:%d 秒", t.Second() - second) // 1s
break
}
}
|
3、Stop 方法停止 Ticker 定时器使用详解
Stop()
方法用于停止定时器。在 Stop 之后,将不再发送更多的 tick 给其通道 C。切记:Stop 操作不会关闭通道 C。
代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second) // 创建一个每秒触发一次的 time.Ticker 对象
quit := make(chanstruct{}) // 创建一个退出通道
// 启动一个新的 goroutine。
// 在这个 goroutine 中,使用 for-select 循环来监听两个事件:
// 定时器的触发(case <-ticker.C)和 退出信号(case <-quit)。
// 每当定时器触发时,它会打印一条消息。如果收到退出信号,它会打印一条消息并退出循环。
go func() {
for {
select {
case <-ticker.C:
fmt.Println("定时器触发啦!")
case <-quit:
fmt.Println("协程停止啦!")
return // 接收到退出信号,退出循环
}
}
}()
time.Sleep(time.Second * 3) // 等待 3 秒,在这期间定时器会触发几次
ticker.Stop() // 停止定时器
time.Sleep(time.Second * 3) // 等待 3 秒,在这期间定时器已经停止,不会再触发
close(quit) // 发送退出信号
fmt.Println("定时器停止啦!")
}
|
Stop 不会关闭其通道 C,因此我们需要借助其他方式(例如退出信号)来清理资源。
1.3 Timer 和 Ticker 的主要区别
用途:
- Timer 用于单次延迟执行任务;
- Ticker 重复执行任务;
行为特点:
- Timer 在设定的延迟时间过后触发一次,发送一个时间值到其通道;
- Ticker 按照设定的间隔周期性地触发,反复发送时间值到其通道;
可控性:
- Timer 可以被重置(Reset 方法)和停止(Stop 方法),Reset 用于改变 Timer 的触发时间;
- Ticker 可以被重置(Reset 方法)和停止(Stop 方法),Reset 用于改变 Ticker 触发的时间间隔;
结束操作:
- Timer 的 Stop 方法用于阻止 Timer 触发,如果 Timer 已经触发,Stop 不会从其通道中删除已发送的时间值;
- Ticker 的 Stop 方法用于停止 Ticker 的周期性触发,一旦停止,它不会再向通道发送新的值;
注意事项:
- 无论是 Timer 还是 Ticker 定时器,调用 Stop 方法之后,并不会关闭它们的 C 通道;如果有其它的 goroutine 在监听这个通道,为避免潜在的内存泄漏,需要手动结束该 goroutine;通常,这种资源释放的问题可以通过使用 context 或通过关闭信号(利用 Channel 实现)来解决;
- 当 Ticker 定时器完成其任务后,为了防止内存泄漏,应调用 Stop 方法来释放相关资源,如果未及时停止 Ticker,可能导致资源持续占用;