Go の context でキャンセルとタイムアウトを制御する
並行処理を行う際、goroutine をいつ終了させるかは重要な問題です。Go の context パッケージは、キャンセル信号やタイムアウトを goroutine 間で伝播させる仕組みを提供します。
context の基本
context は親から子へ伝わる「コンテキスト」を表します。親がキャンセルされると、その子もすべてキャンセルされます。
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
fmt.Println(ctx)
}context.Background() はルートとなる空のコンテキストを返します。これを起点に、キャンセルやタイムアウト付きのコンテキストを派生させていきます。
WithCancel で手動キャンセル
context.WithCancel は、手動でキャンセルできるコンテキストを作成します。
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("キャンセルされました")
return
default:
fmt.Println("処理中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // ここでキャンセル
time.Sleep(1 * time.Second)
}cancel() を呼ぶと、ctx.Done() チャネルがクローズされ、goroutine 内の select がそれを検知します。
WithTimeout で自動タイムアウト
context.WithTimeout は、指定時間が経過すると自動的にキャンセルされるコンテキストを作成します。
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
select {
case <-time.After(5 * time.Second):
fmt.Println("処理完了")
case <-ctx.Done():
fmt.Println("タイムアウト:", ctx.Err())
}
}この例では 3 秒でタイムアウトするため、5 秒かかる処理は完了せず「タイムアウト」が出力されます。defer cancel() は、タイムアウト前に処理が終わった場合にリソースを解放するために必要です。
WithDeadline で期限を指定
context.WithDeadline は、特定の時刻を期限として設定します。
func main() {
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("処理完了")
case <-ctx.Done():
fmt.Println("期限切れ:", ctx.Err())
}
}WithTimeout と似ていますが、相対的な時間ではなく絶対的な時刻を指定する点が異なります。
実践的な使い方:HTTP リクエストのタイムアウト
context は HTTP クライアントなど、標準ライブラリの多くの場所で使われています。
func fetchWithTimeout(url string) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Println("ステータス:", resp.Status)
return nil
}http.NewRequestWithContext を使うことで、リクエストにタイムアウトを設定できます。サーバーが 5 秒以内に応答しなければ、リクエストは自動的にキャンセルされます。
context を関数に渡す慣習
Go では、context を受け取る関数は第一引数に ctx context.Context を置くのが慣習です。
func doWork(ctx context.Context, data string) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
// 処理を実行
fmt.Println("処理:", data)
return nil
}
}この慣習に従うことで、キャンセル可能な関数であることが一目でわかります。