コンテンツにスキップ

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)
}

ベストプラクティス

  1. Contextは第一引数に渡す: func DoSomething(ctx context.Context, ...) がGoの慣習
  2. structに入れない: Contextはリクエストスコープなので、フィールドに持たせると混乱する
  3. 必ずcancelを呼ぶ: defer cancel() を忘れるとgoroutineリークが起きる
  4. nil Contextを渡さない: context.Background()context.TODO() を使う
  5. Valueは型安全なキーを使う: stringint をキーにすると衝突する。専用の型を定義する

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のクエリも中断される。