Go の time.Ticker で定期処理を並行実行する
time.Ticker は一定間隔で値を送信し続ける channel を提供します。定期的なポーリング、ヘルスチェック、メトリクス収集など、繰り返し処理を並行で実行する場面で活躍します。
time.Ticker の基本
time.NewTicker で指定した間隔ごとに現在時刻を送信する Ticker を作成します。
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 必ず Stop を呼ぶ
count := 0
for t := range ticker.C {
count++
fmt.Println("tick:", t.Format("15:04:05"))
if count >= 5 {
break
}
}
}ticker.C は受信専用の channel で、指定した間隔ごとに時刻が送られてきます。使い終わったら Stop を呼んでリソースを解放することが重要です。
time.Tick との違い
time.Tick は Ticker を返さず channel だけを返す簡易版です。ただし Stop できないため、プログラムの終了まで動き続けます。
Ticker 構造体を返す。Stop でいつでも停止できる。長時間動くプログラムに適切。
channel だけを返す。停止手段がない。main 関数で短時間だけ使う場合のみ許容される。
基本的には time.NewTicker を使い、必ず Stop を呼ぶようにしましょう。
定期処理の goroutine パターン
バックグラウンドで定期処理を行う goroutine を作成するパターンです。
package main
import (
"fmt"
"time"
)
func startPeriodicTask(interval time.Duration, done <-chan struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fmt.Println("定期処理を実行:", time.Now().Format("15:04:05"))
case <-done:
fmt.Println("定期処理を停止")
return
}
}
}
func main() {
done := make(chan struct{})
go startPeriodicTask(500*time.Millisecond, done)
// 3秒後に停止
time.Sleep(3 * time.Second)
close(done)
time.Sleep(100 * time.Millisecond)
fmt.Println("終了")
}done channel を使って外部から停止を指示できるようにしています。select で ticker.C と done の両方を待つことで、定期実行と停止通知の両方に対応します。
実践例:ヘルスチェック
定期的にサービスの状態を確認する例です。
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
func checkHealth() bool {
// 実際は HTTP リクエストやDB接続確認など
return rand.Intn(10) > 1 // 90% の確率で正常
}
func healthChecker(ctx context.Context, interval time.Duration) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if checkHealth() {
fmt.Println("✓ 正常")
} else {
fmt.Println("✗ 異常検出!")
}
case <-ctx.Done():
fmt.Println("ヘルスチェック終了")
return
}
}
}
func main() {
rand.Seed(time.Now().UnixNano())
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go healthChecker(ctx, 1*time.Second)
<-ctx.Done()
time.Sleep(100 * time.Millisecond)
}context を使うことで、タイムアウトや明示的なキャンセルに対応しています。
複数の Ticker を並行実行
異なる間隔で複数の定期処理を同時に実行することもできます。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ticker1 := time.NewTicker(1 * time.Second)
ticker2 := time.NewTicker(2500 * time.Millisecond)
defer ticker1.Stop()
defer ticker2.Stop()
for {
select {
case <-ticker1.C:
fmt.Println("タスクA(1秒間隔)")
case <-ticker2.C:
fmt.Println("タスクB(2.5秒間隔)")
case <-ctx.Done():
fmt.Println("終了")
return
}
}
}1つの select で複数の Ticker を待つことで、それぞれの間隔で処理が実行されます。
Reset で間隔を変更する
Go 1.15 以降では Reset メソッドで Ticker の間隔を動的に変更できます。
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
count := 0
for t := range ticker.C {
count++
fmt.Println("tick:", t.Format("15:04:05"))
if count == 3 {
fmt.Println("-- 間隔を 500ms に変更 --")
ticker.Reset(500 * time.Millisecond)
}
if count >= 7 {
break
}
}
}実行中に処理頻度を調整したい場合に便利です。負荷状況に応じてポーリング間隔を動的に変えるといった用途で使えます。