go-details

context.Contextとは、その処理の実行文脈を下流へ渡すための仕組み

0 likes

context.Contextについて

Go を書き始めると、かなり早い段階で context.Context に出会います。
HTTP ハンドラでも、DB アクセスでも、外部 API 呼び出しでも、たいていの関数が ctx context.Context を受け取っています。

最初は「これは何のためにあるのか」「キャンセル用なのか」「毎回渡す必要があるのか」が分かりにくいかもしれません。
この記事では、context.Context を実務でどう捉えると理解しやすいかを整理します。

ひとことで言うと

context.Context は、その処理の実行文脈を下流へ渡すための仕組みです。

ここでいう実行文脈には、主に次のような情報が含まれます。

  • この処理はいつ打ち切るべきか
  • 親の処理が中断されたら、子の処理も止めるべきか
  • このリクエストにひもづく値は何か

つまり context.Context は、単なる「引数」ではなく、処理全体のライフサイクルをそろえるための仕組みです。

「止めるためだけ」のものではない

context.Context はよくキャンセルやタイムアウトの文脈で説明されます。
それは正しいのですが、それだけでは少し足りません。

context.Context の代表的な役割は次の3つです。

1. キャンセルを伝える

親の処理が不要になったら、子の処理にも「もう続けなくていい」と伝えられます。

たとえば HTTP リクエストが途中で切断された場合、そのリクエストの中で動いていた DB クエリや外部 API 呼び出しも止めたいことがあります。
そういうときに Context が使われます。

2. タイムアウトや締切を伝える

「この処理は 3 秒以内で終えてほしい」「15 秒たっても終わらなければ打ち切る」といった制約を渡せます。

たとえば次のコードです。

この ctx を下流の関数に渡すと、その処理は 15 秒を超えた時点でキャンセル扱いになります。

3. リクエストスコープの値を渡す

認証情報、トレース ID、リクエスト ID のように、ある1回の処理にだけひもづく値を渡せます。

たとえば HTTP リクエストの開始時にリクエスト ID を Context に入れておき、ログ出力や下流の関数で同じ ID を参照する、といった使い方です。

ただし、この用途は便利な反面、乱用もしやすいので注意が必要です。
この点は後で触れます。

なぜ毎回渡すのか

Go のライブラリでは、I/O を伴う処理が Context を受け取る設計になっていることが多いです。

たとえば次のような処理です。

  • DB クエリ
  • HTTP クライアントの送信
  • RPC 呼び出し
  • ストリーム処理
  • キューやサブスクリプションの待機

これらはいつ終わるか分からず、外部要因で遅くなることもあります。
だから呼び出し元が「もう待たない」「親が止まったので子も止める」と制御できるようにしてあります。

つまり ctx を渡すのは、単に Go のお作法だからではなく、呼び出し元が処理の寿命を管理するためです。

具体例: DB 接続確認での利用

たとえば PostgreSQL の接続確認で次のように書きます。

ここで pool.Ping(ctx)ctx を受け取るのは、接続確認にもタイムアウトやキャンセルを効かせたいからです。

もし DB 側が応答しなければ、ずっと待ち続けるのではなく、15 秒で打ち切れます。

この考え方は Ping に限らず、クエリ実行にもそのまま当てはまります。

この ctx も「この取得処理は、親の処理の制約に従って動いてほしい」という意味を持っています。

context.Background()context.TODO()

Context の起点としてよく出てくるのが次の2つです。

context.Background()

空の親コンテキストです。
期限もキャンセルも値も持っていません。

アプリの起点や main 関数などで、最初の親として使うのが一般的です。

context.TODO()

まだどの Context を使うべきか決めきれていないときの仮置きです。

開発途中では便利ですが、最終的には適切な親コンテキストへ置き換えるほうがよいです。

よく使う関数

context.WithCancel

明示的に止められる Context を作ります。

必要になったタイミングで cancel() を呼ぶと、その ctx とそこから派生した子 Context にキャンセルが伝わります。

context.WithTimeout

一定時間で自動キャンセルされる Context を作ります。

短時間で終わるべき I/O によく使われます。

context.WithDeadline

「何秒後」ではなく、「この時刻まで」という締切を指定します。

context.WithValue

Context に値を入れます。

これは便利ですが、使いどころを誤ると設計が崩れやすいです。

Context に入れてよいもの、だめなもの

Context は何でも入れてよい汎用コンテナではありません。

入れてよいのは、その1回の処理にひもづく値です。

たとえば次のようなものです。

  • リクエスト ID
  • トレース ID
  • 認証済みユーザー情報
  • ログ出力に必要な相関 ID

逆に、次のようなものは入れるべきではありません。

  • DB 接続プール
  • アプリ設定
  • ロガーそのもの
  • 接続文字列
  • あちこちから使う常設の依存オブジェクト

これらは処理ごとの一時的な情報ではなく、アプリケーションの構成要素です。
Context に押し込むと依存関係が見えにくくなり、関数シグネチャも読みにくくなります。

よくあるアンチパターン

1. Context を struct のフィールドに持たせる

Context は通常、関数呼び出しごとに渡します。
長生きする struct に保持すると、どの処理の文脈なのか曖昧になります。

悪い例:

これは避けたほうがよいです。

2. 省略可能引数の代わりに使う

Context にフラグや設定値を何でも入れ始めると、関数の依存関係が見えなくなります。

たとえば「詳細ログを出すか」「このモードを有効にするか」といったアプリ設定を Context に入れるのは不適切です。

3. cancel() を呼ばない

WithCancelWithTimeout を使ったのに cancel を呼ばないと、関連リソースの解放が遅れることがあります。

基本はこうです。

4. 何でも context.Background() から始める

本来は親の Context を引き継ぐべき場所でも、毎回 context.Background() を作ってしまうと、キャンセルや締切の伝播が切れます。

たとえば HTTP リクエストの中で処理しているのに、そこで新しく Background() を作ると、クライアントが切断しても下流処理が止まらなくなることがあります。

実務での理解のしかた

context.Context を理解するうえで大事なのは、これを「キャンセル用 API」とだけ見ないことです。

むしろ次のように理解すると実務では扱いやすくなります。

Context は、その処理がどの親から始まり、どこまで生きてよく、どういう制約を持つかを表すもの。

この見方をすると、ctx を引数で受け取る理由も自然に分かります。
その関数自身が主役なのではなく、呼び出し元の実行文脈の中で動いているからです。

まとめ

context.Context は、Go における実行制御の共通基盤です。

ポイントを整理すると次の通りです。

  • Context は実行文脈を下流へ渡す
  • 主な役割はキャンセル、タイムアウト、リクエストスコープの値
  • I/O を伴う処理では特に重要
  • 値の格納は乱用しない
  • Context は関数に渡して使い、長生きする場所に持たせない

context.Context は最初は少し抽象的ですが、HTTP、DB、外部 API などをまたぐ処理を書き始めると、その必要性がよく分かります。
「この処理は、どの文脈の中で、どこまで生きてよいか」をそろえるための仕組みだと捉えると、かなり見通しがよくなります。