読者です 読者をやめる 読者になる 読者になる

golangのよくあるtickerのサンプルがイケてない件

golangでのtickerのサンプルで以下のようにgoroutine内でrangeを使ってforループを回すのをよく見かけます。

package main

import "time"
import "fmt"

func main() {
    ticker := time.NewTicker(10 * time.Millisecond)
    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at", t)
        }
    }()

    time.Sleep(35 * time.Millisecond)
    ticker.Stop()
}

コレ、以下のようにちょっと変更して実行してみましょう。

package main

import "time"
import "fmt"

func main() {
    ticker := time.NewTicker(10 * time.Millisecond)
    go func() {
        for t := range ticker.C {
            fmt.Println("Tick at", t)
        }
        fmt.Println("Reachable?")
    }()

    time.Sleep(35 * time.Millisecond)
    ticker.Stop()
    time.Sleep(100 * time.Millisecond)
}

"Reachable?"とは表示してくれませんね?

tickerのStop()してもチャンネルをcloseしてはくれないので、残念ながらこのforループは抜けることはないのです。 その結果、goroutineがリークしてしまいます。

んじゃあどうすればいいかっつーと、以下のようにする必要があります。

package main

import "time"
import "fmt"

func main() {
    ticker := time.NewTicker(10 * time.Millisecond)
    stop := make(chan bool)
    go func() {
    loop:
        for {
            select {
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            case <-stop:
                break loop
            }
        }
        fmt.Println("Reachable!")
    }()

    time.Sleep(35 * time.Millisecond)
    ticker.Stop()
    close(stop)
    time.Sleep(100 * time.Millisecond)
}

……別でチャンネル用意したりとか正直クソメンドクサイですね。
ただ単にバックグラウンドでの繰り返し処理をするためだけに毎回こんなコード書くとか耐えられません。

ということで、ラッパーをつくりました。

https://github.com/okzk/ticker

このラッパーを使えば以下のようになります。

package main

import (
    "fmt"
    "github/okzk/ticker"
    "time"
)

func main() {
    ticker := ticker.New(10*time.Millisecond, func(t time.Time) {
        fmt.Println("Tick at", t)
    })

    time.Sleep(35 * time.Millisecond)
    ticker.Stop()
}

ラクチン!