Golang の Context: 過小評価されている重要機能
Contextとは何か¶
context.Context はGoの標準ライブラリで、リクエストスコープの値・キャンセル信号・デッドライン を関数間で伝搬するためのインターフェース。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
なぜ重要か¶
HTTPリクエストを受けてDBやAPIを呼び出す処理を考える。
- クライアントが接続を切った → 処理を続けても無意味。途中でキャンセルしたい
- タイムアウトを設けたい → 3秒以内に終わらなければ中断したい
- リクエストIDをログに含めたい → 全関数に引数で渡すのは煩雑
これをContextが解決する。
基本的な使い方¶
キャンセル¶
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 必ずcancelを呼ぶ(リソースリーク防止)
go func() {
select {
case <-ctx.Done():
fmt.Println("キャンセルされた:", ctx.Err())
return
case result := <-doWork():
fmt.Println("完了:", result)
}
}()
// 何らかの条件でキャンセル
cancel()
タイムアウト¶
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// タイムアウトした場合もここでキャッチできる
fmt.Println(err) // context deadline exceeded
}
値の伝搬¶
type key string
const requestIDKey key = "requestID"
// Contextに値をセット
ctx = context.WithValue(ctx, requestIDKey, "req-abc-123")
// 別の関数で取り出す
func handler(ctx context.Context) {
reqID := ctx.Value(requestIDKey).(string)
log.Printf("request_id=%s processing...", reqID)
}
ベストプラクティス¶
- Contextは第一引数に渡す:
func DoSomething(ctx context.Context, ...)がGoの慣習 - structに入れない: Contextはリクエストスコープなので、フィールドに持たせると混乱する
- 必ずcancelを呼ぶ:
defer cancel()を忘れるとgoroutineリークが起きる - nil Contextを渡さない:
context.Background()かcontext.TODO()を使う - Valueは型安全なキーを使う:
stringやintをキーにすると衝突する。専用の型を定義する
HTTPサーバーでの実例¶
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // HTTPリクエストのContextを取得
user, err := h.db.FindUser(ctx, userID) // DBにContextを渡す
if err != nil {
if errors.Is(err, context.Canceled) {
// クライアントが接続を切った
return
}
http.Error(w, err.Error(), 500)
return
}
// ...
}
クライアントが接続を切ると r.Context() のDoneチャンネルが閉じ、DBのクエリも中断される。