Go の select と time.After でタイムアウトを実装する
channel からの受信を待つ処理で、一定時間内に応答がなければ諦めたいことがあります。select と time.After を組み合わせることで、シンプルにタイムアウトを実装できます。
time.After の基本
time.After は指定した時間が経過すると値を送信する channel を返します。
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("開始")
<-time.After(2 * time.Second)
fmt.Println("2秒経過")
}単独で使うと time.Sleep と同じですが、select と組み合わせることで真価を発揮します。
select でタイムアウトを実装する
select は複数の channel 操作を同時に待ち、最初に準備できたものを実行します。処理結果の channel と time.After の channel を同時に待つことで、タイムアウトを実現できます。
package main
import (
"fmt"
"time"
)
func slowOperation() <-chan string {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second) // 重い処理をシミュレート
ch <- "処理完了"
}()
return ch
}
func main() {
result := slowOperation()
select {
case msg := <-result:
fmt.Println(msg)
case <-time.After(2 * time.Second):
fmt.Println("タイムアウト")
}
}この例では処理に3秒かかりますが、タイムアウトは2秒なので「タイムアウト」が出力されます。
実践例:API リクエストのタイムアウト
外部 API を呼び出す際にタイムアウトを設定する例です。
package main
import (
"fmt"
"math/rand"
"time"
)
func fetchData(id int) <-chan string {
ch := make(chan string)
go func() {
// ランダムな遅延(0〜3秒)
delay := time.Duration(rand.Intn(3000)) * time.Millisecond
time.Sleep(delay)
ch <- fmt.Sprintf("データ%d(%v で取得)", id, delay)
}()
return ch
}
func main() {
rand.Seed(time.Now().UnixNano())
for i := 1; i <= 5; i++ {
ch := fetchData(i)
select {
case data := <-ch:
fmt.Println("成功:", data)
case <-time.After(1 * time.Second):
fmt.Printf("リクエスト%d: タイムアウト\n", i)
}
}
}各リクエストに1秒のタイムアウトを設定しています。1秒以内に応答があれば成功、なければタイムアウトとなります。
複数の channel とタイムアウト
複数の処理を並行して待ちつつ、全体にタイムアウトを設ける場合です。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "ch1 完了"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "ch2 完了"
}()
timeout := time.After(3 * time.Second)
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
case <-timeout:
fmt.Println("全体タイムアウト")
return
}
}
fmt.Println("すべて完了")
}time.After をループの外で一度だけ呼ぶことで、全体で3秒という制限時間を設けています。
time.After の注意点
time.After はループ内で呼ぶと毎回新しいタイマーが作られます。
毎回新しいタイマーが生成される。各イテレーションごとにタイムアウトをリセットしたい場合に適切。
1つのタイマーを共有する。全体の制限時間を設けたい場合に適切。
また、タイムアウトしても goroutine 自体は止まりません。リソースリークを防ぐには context を使った適切なキャンセル処理が必要です。
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) <-chan string {
ch := make(chan string)
go func() {
select {
case <-time.After(3 * time.Second):
ch <- "処理完了"
case <-ctx.Done():
fmt.Println("キャンセルされました")
return
}
}()
return ch
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
select {
case result := <-doWork(ctx):
fmt.Println(result)
case <-ctx.Done():
fmt.Println("タイムアウト")
}
time.Sleep(100 * time.Millisecond) // goroutine の出力を待つ
}time.After は簡易的なタイムアウトに便利ですが、本格的なキャンセル処理には context を使うのがベストプラクティスです。