Go 定時器:Timer 和 Ticker
前言
在日常開發中,我們可能會遇到需要延遲執行或週期性地執行一些任務。這個時候就需要用到 Go
語言中的定時器。
在 Go
語言中,定時器類型有兩種:time.Timer
一次性定時器和 time.Ticker
週期性定時器。本文將會對這兩種定時器類型進行介紹。
準備好了嗎?準備一杯你最喜歡的咖啡或茶,隨着本文一探究竟吧。
Timer:一次性定時器
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
來創建定時器以及定時器的基本用法:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/usage.go
package main
import (
"fmt"
"time"
)
func main() {
// 使用 NewTimer 創建一個定時器
timer := time.NewTimer(time.Second)
gofunc() {
select {
case <-timer.C:
fmt.Println("timer 定時器觸發啦!")
}
}()
// 使用 AfterFunc 創建另一個定時器
time.AfterFunc(time.Second, func() {
fmt.Println("timer2 定時器觸發啦!")
})
// 主goroutine等待兩秒,確保看到定時器觸發的輸出
time.Sleep(time.Second * 2)
}
代碼運行結果如下所示:
timer 定時器觸發啦!
timer2 定時器觸發啦!
下面是代碼的逐步解析:
-
首先使用
NewTimer
創建了一個定時器,然後在一個新的goroutine
中監聽它的C
屬性以等待定時器觸發。 -
其次,使用
AfterFunc
創建另一個定時器,通過指定一個 回調函數 來處理定時器到期事件。 -
最後,主
goroutine
等待足夠長的時間以確保定時器的觸發信息能夠被打印出來。
方法詳解
Reset
Reset(d Duration) bool
:該方法用於重置 Timer
定時器的過期時間,也可以理解爲重新激活定時器。它接受一個 time.Duration
類型的參數 d
,表示定時器在過期之前等待的時間。
除此之外,該方法還返回一個 bool
值:
-
如果定時器處於活動的狀態,返回
true
。 -
如果定時器已經過期或被停止了,返回
false
(false
並不意味着激活定時器失敗,只是標識定時器的當前狀態)。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/reset.go
package main
import (
"fmt"
"time"
)
func main() {
timer := time.NewTimer(5 * time.Second)
// 第一次重置,定時器處於激活狀態,因此返回 true
b := timer.Reset(1 * time.Second)
fmt.Println(b) // true
second := time.Now().Second()
select {
case t := <-timer.C:
fmt.Println(t.Second() - second) // 1s
}
// 第二次重置,定時器已經處於過期狀態,因此返回 false
b = timer.Reset(2 * time.Second)
fmt.Println(b) // false
second = time.Now().Second()
select {
case t := <-timer.C:
fmt.Println(t.Second() - second) // 2s
}
}
代碼運行結果如下所示:
true
1
false
2
下面是代碼的逐步解析:
-
首先,創建了一個定時器,設置爲 5 秒後到期。
-
然後調用
Reset
方法立即將其重置爲 1 秒後到期。因爲此時定時器仍處於激活狀態(即還未到期),所以Reset
方法返回true
。 -
接下來的
select
語句等待定時器到期,並打印出實際經過的秒數(約等於 1 秒)。 -
接着第二次重置定時器,這次設置爲 2 秒後到期。由於定時器在這次重置時已經到期,
Reset
方法返回false
。 -
最後,再次使用
select
語句等待定時器到期,並打印出這次經過的秒數(約等於 2 秒)。
Stop
Stop() bool
:該方法用於停止定時器。如果定時器停止成功,返回 true
,如果定時器已經過期或被停止,則返回 false
。切記:Stop
操作不會關閉通道 C
。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/timer/stop.go
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
}
代碼運行結果如下所示:
true
false
下面是代碼的逐步解析:
-
首先,創建了一個設置爲 3 秒後觸發的定時器。
-
然後立即調用
Stop
方法停止定時器。因爲此時定時器還未觸發,所以Stop
返回true
。 -
最後再次調用
Stop
方法嘗試停止同一個定時器。由於定時器已經被停止,這次Stop
返回false
。
Ticker:週期性定時器
Tciker
是一個週期性的定時器,用於在固定的時間間隔重複執行任務。它在每個間隔時間到來時,向其通道(Channel
)發送當前時間。
基本使用
我們可以使用 NewTicker
函數來創建一個新的 Ticker
對象,該函數接受一個 time.Duration
類型的參數 d
(時間間隔)。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/usage.go
package main
import (
"context"
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*5)
defer cancelFunc()
gofunc() {
for {
select {
case <-timeout.Done():
fmt.Println("timeout done")
return
case <-ticker.C:
fmt.Println("定時器觸發啦!")
}
}
}()
// 主goroutine等待 7 秒,確保看到定時器觸發的輸出
time.Sleep(time.Second * 7)
}
代碼運行結果如下所示:
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
timeout done
下面是代碼的逐步解析:
-
首先,創建了一個每秒觸發的定時器,確保函數週期結束後清理定時器,我們應該加上
defer ticker.Stop()
-
然後,創建一個在 5 秒後超時的上下文。
cancelFunc
被用於在退出前清理上下文。 -
接着,在一個新的
goroutine
中,select
語句用於監聽兩個通道:定時器的通道 (ticker.C
) 和超時上下文的完成通道 (timeout.Done()
)。當定時器每秒觸發時,會打印出消息。當上下文超時(即 5 秒過後),打印出超時信息,並返回從而結束該goroutine
。 -
最後,主
goroutine
通過time.Sleep(time.Second * 7)
等待 7 秒,以確保能夠觀察到定時器觸發和超時事件的輸出。
除了使用 select
語句監聽 ticker.C
以外,我們還可以使用 for range
的形式進行監聽:
for range ticker.C {}
需要注意的是,即使通過 Stop
方法停止 Ticker
定時器,其 C
通道不會被關閉。這意味着無論是通過 for select
還是 for range
去監聽 ticker.C
,我們需要使用其他機制來退出循環,例如使用 context
上下文。
方法詳解
Reset
Reset(d Duration)
方法用於停止計時器並將其週期重置爲指定的時間。下一個時間刻度將在新週期結束後生效。它接受一個 time.Duration
類型的參數 d
,表示新的週期。該參數必須大於零;否則 Reset
方法內部將會 panic
。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/reset.go
package main
import (
"time"
)
func main() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
// 重置定時器
ticker.Reset(1 * time.Second)
second := time.Now().Second()
for t := range ticker.C {
// 1s
fmt.Printf("週期:%d 秒", t.Second()-second)
break
}
}
代碼運行結果如下所示:
週期:1 秒
下面是代碼的逐步解析:
-
首先,創建了一個每 5 秒觸發一次的定時器
time.Ticker
。 -
其次,使用
Reset
方法重置定時器的觸發間隔。5 秒變成 1 秒。 -
最後通過一次循環,打印定時器的週期,預期結果爲 1 秒。
Stop
Stop()
方法用於停止定時器。在 Stop
之後,將不再發送更多的 tick
給其通道 C
。切記:Stop
操作不會關閉通道 C
。
下面是代碼示例:
// https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer/ticker/stop.go
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(time.Second)
quit := make(chanstruct{}) // 創建一個退出通道
gofunc() {
for {
select {
case <-ticker.C:
fmt.Println("定時器觸發啦!")
case <-quit:
fmt.Println("協程停止啦!")
return// 接收到退出信號,退出循環
}
}
}()
time.Sleep(time.Second * 3)
ticker.Stop() // 停止定時器
close(quit) // 發送退出信號
fmt.Println("定時器停止啦!")
}
代碼運行結果如下所示:
定時器觸發啦!
定時器觸發啦!
定時器觸發啦!
協程停止啦!
定時器停止啦!
-
首先,創建一個每秒觸發一次的
time.Ticker
對象。同時,引入了一個類型爲chan struct{}
的退出通道 quit。這個通道將用於向運行中的goroutine
發送停止信號。 -
其次,啓動一個新的
goroutine
。在這個goroutine
中,使用for-select
循環來監聽兩個事件:定時器的觸發(case <-ticker.C
)和退出信號(case <-quit
)。每當定時器觸發時,它會打印一條消息。如果收到退出信號,它會打印一條消息並退出循環。 -
接着,在主
goroutine
中,time.Sleep(time.Second * 3)
模擬了一段等待時間(3 秒),在這期間定時器會觸發幾次。 -
最後,主
goroutine
通過調用Stop
方法停止定時器,然後關閉退出通道。goroutine
接收到退出信號後打印出一條消息並退出循環。
Stop
不會關閉其通道 C
,因此我們需要藉助其他方式(例如退出信號)來清理資源。
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
,可能導致資源持續佔用。
小結
本文深入探討了 Go
語言中的 Timer
和 Ticker
定時器,詳細介紹了它們的創建方式、基本用法以及相關的方法等。此外,文章還概括了這兩個定時器之間的主要區別,並強調了在使用過程中的注意事項。
在編寫 Go
代碼時,我們應根據不同的應用場景去選擇合適的定時器。同時,我們應遵循良好的規範,特別是在定時器使用完畢後及時釋放資源,對於避免潛在的內存泄漏問題尤爲重要。
文章裏的所有代碼已上傳至:https://github.com/chenmingyong0423/blog/blob/master/tutorial-code/go/timer
”
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/IgXFofLEwhYYYUtIZ4_rTA