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 できないため、プログラムの終了まで動き続けます。

time.NewTicker

Ticker 構造体を返す。Stop でいつでも停止できる。長時間動くプログラムに適切。

time.Tick

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 を使って外部から停止を指示できるようにしています。selectticker.Cdone の両方を待つことで、定期実行と停止通知の両方に対応します。

実践例:ヘルスチェック

定期的にサービスの状態を確認する例です。

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
        }
    }
}

実行中に処理頻度を調整したい場合に便利です。負荷状況に応じてポーリング間隔を動的に変えるといった用途で使えます。