context.WithTimeoutの仕組み
指定した時間が経過すると自動的にキャンセルされる Context を生成する関数
内部的には WithDeadline の薄いラッパーであり、現在時刻 + duration を deadline として設定し、その時刻になったらキャンセル関数が呼ばれるようになっています。
このタイムアウト context(通称 timeoutCtx)は、HTTP リクエストや DB クエリのようにキャンセルをサポートする処理に渡すことで、「待ちすぎ問題」を防ぎます。
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
timeoutCtx が内部で何をしているか
時間内に終わらなかった時の内部処理
- 新しく “タイムアウト付きの箱” をつくります
まず、普通のcontext(親)を元にして「期限つきの新しいcontext」 を作ります - いつまでに終わるべきか” という時間を覚えておきます
そのcontextの中に、「○秒後にタイムアウトする」 という時間を記録します。 - タイマーを動かして “期限の時間” を待ちます
内部でタイマーをセットして、「期限になったかどうか」を監視します。 - 期限の時間に到達したら cancel 関数を発火します
時間になった瞬間、contextが「キャンセルされた」という合図を出します。 -
contextを使っている処理へ “終わりだよ” と知らせます
ctx.Done()が閉じる
ctx.Err()がDeadlineExceededになる - キャンセルはその context を使っているすべての子処理に伝播
Go のcontextは “木のような構造” になっているので、一度キャンセルされると 関連する処理全てに伝わります。
時間内に終わった時の内部処理
-
context.WithTimeoutがタイマーを仕込みます - 処理が成功します
-
defer cancel()が呼ばれます -
cancel()がタイマーを止めます -
contextは“正常終了”扱い - context は“普通に終わった”ので、キャンセル状態にはならないです
ctx.Err()→nilのまま
ctx.Done()→ 閉じない
つまり「期限内に終わったので、キャンセルは発生していない」という状態になります。
WithTimeout を使わないと起きること
- 外部 API や DB が不調のとき「無限に待つ」
- HTTP クライアントや DB ドライバの処理は、デフォルトで無制限に待つものが多いです。
タイムアウトがないと、goroutine が溜まっていき、最悪リソース枯渇になります。
- HTTP クライアントや DB ドライバの処理は、デフォルトで無制限に待つものが多いです。
- goroutine リーク
- 処理が永遠に終わらなければ、関連する goroutine は回収されません
- サービス全体の遅延
- 待ち続けるリクエストが増えるほど、スレッドプールや DB コネクションが枯れ、他の正常な処理まで遅延します
WithTimeout を使うべき場面・使うべきでない場面
使うべき場面
- goroutine 内の I/O 操作の最大待ち時間を決めたいとき
- HTTP リクエスト
- DB クエリ
- ファイル読み書き(context 対応のもの)
- Redis など
- select で ctx.Done() を見ている goroutine
- データベース、HTTP、gRPC 等の外部呼び出し
- “何秒以内に終わらなければ諦める” が業務要件にあるとき
使うべきでない場面
- goroutine を「止めたいだけ」のとき
- selfチェック(ctx.Done())をしない goroutine は止まらない。
- CPU バウンド処理
- context を見ない処理
- タイムアウトを誤用してロジックが複雑化するケース
まとめ
context.WithTimeout は、外部 API や DB など「キャンセルをサポートする処理」に対して、時間切れで確実に中断できるようにするための仕組みです。
一方で、context をチェックしない goroutine や CPU バウンド処理に対しては効果がなく、誤用するとロジックを複雑化させるだけなので注意が必要です。