はじめに
Goを書いていると、関数の引数にやたらと ctx(context.Context)が登場します。
func GetUser(ctx context.Context, id string) (*User, error) {
...
}
公式パッケージの多くにも ctx が出てきますし、実務でも「とりあえず入れておく」ように書かれていることがよくあります。
しかし、
ctxって結局何なの?
何ができるの?
なぜ引数で受け渡す必要があるの?
どんな時に必要なの?
…と疑問に思っている人も多いのではないでしょうか。
私自身Goを学び始めた当初は「とりあえず大体の関数につけておけばいいんだな」くらいに思っていて、あまり深く考えてこなかったので、これを機に整理してみました
そもそもctxとは何か?
結論から言うと、Contextは “処理のキャンセルやデッドライン、リクエストスコープの値を伝播するための仕組み” です。
つまり、処理がいつ終わるべきか・途中で止めるべきか・その処理に紐づく情報は何か
をコールスタック全体で共有するための仕組みです。
GoのAPIサーバーを立てた時、リクエストが来るたびに軽量スレッド(goroutine)を大量に生成し処理を行います。
が、その裏返しで
「どのタイミングで処理を中断すべきか」
を管理する仕組みがなければ、goroutineが無限に生き残る危険があります。
Contextがないと起きる問題
- HTTPリクエストがキャンセルされても処理が走り続ける
- DBアクセスがハングしても goroutine が永遠に残る
- 親処理が終わっても子goroutineが残り続け、リークする
- APIサーバーの負荷が増え続ける
Context主な機能
1. キャンセルの伝播
親のContextがキャンセルされると
そのContextを受けたすべての処理(goroutine)が中断される という仕組みです
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
これを使うことでGoのAPIサーバーでは、クライアントが接続を切ると
HTTPサーバーがContextをキャンセルして、下流の処理(DB・外部API)にも中断を伝える
という動きをすることができます。
2.Deadline / Timeout の設定
「◯秒で終わらなかったらキャンセルしてね」という制御をすることができます
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
例えばリクエスト全体で1分以上かかったら中断する、といった用途です。
バッチ系の処理など長時間の処理になりそうな場合は「1時間経ったらで一旦処理を中断する」みたいなことができるようになります
3. リクエストスコープの値を渡す
Contextは "値を運べる" という特徴もあります。
ctx = context.WithValue(ctx, "requestID", "abc-123")
しかし公式は WithValue の濫用を推奨していません。
一般的には以下の用途に限定されます:
- ログに必要な requestID
- 認証情報
- トレーシング(OpenTelemetry など)
“何でも入れていい箱” ではない点は注意する必要があります。
実例
HTTPハンドラの例が最もわかりやすいです。
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // リクエストに紐づくContextを取得
user, err := s.repo.GetUser(ctx, id)
}
このようにハンドラでcontextを定義することで
- クライアントが切断 → Contextキャンセル
- サーバー側タイムアウト → Contextキャンセル
を自動で行い、ctxが下層の処理に伝播されるだけで
下層の処理全体を自動的に中断させることができるようになります。
関数を跨って処理の制御を行うという意味では
「とりあえず大体の関数につけておけばいいんだな」
は割と間違ってないのかもしれません!
おわりに
最後まで読んでくださりありがとうございます!
Go学びたての頃は「goroutineが残り続ける!?」みたいな、
普段あまり意識しないことが後々の障害につながるとはつゆ知らず。。。
新人さんに「ctxって何ですか?とりあえず大体の関数につけておけばいいんですか?」ともし質問されたら
この辺の背景も含めて説明できればと思います💪
参考