Go: Goのコンテキスト:WithCancel、WithTimeout、WithDeadline、WithValue、チ…

最終更新:2026-08-26

コンテキストは、Goにおける並行処理制御の要です。これにより、タイムアウト、キャンセル、および値が、goroutineの呼び出しチェーンを通じて円滑に渡されるようになります。

サービスが数十もの外部依存関係を同時に処理する必要がある場合(それぞれに独自のタイムアウトやキャンセルロジックがある)、それらを一貫性のある方法で管理するにはどうすればよいでしょうか?このレッスンでは、Goのcontextパッケージの包括的な活用方法を学びます。

1. 学習内容



2. バックエンドエンジニアの実話

(1) 課題:上流のコンポーネントの1つでタイムアウトが発生すると、システム全体がクラッシュしてしまう

Xiaoliは注文システムのエンジニアです。彼女は、以下の3つの下流サービスに依存するREST APIを担当しています:

「注文処理APIは、在庫サービス、決済サービス、通知サービスの3つのサービスを呼び出していました。ある日、在庫サービスの応答に20秒かかってしまい、すべてのgoroutineがその応答を待っていたため、私のサービスのメモリ使用量が急増しました。その結果、他のユーザーからのリクエストも処理できなくなってしまいました。上司から『注文ページが完全にダウンしているのはなぜだ?』と尋ねられました。」

問題の分析:

GO
// Bad code: no timeout control
func PlaceOrder(ctx context.Context, order Order) error {
    // If InventoryCheck hangs for 30 seconds, the goroutine waits needlessly for 30 seconds
    ok, err := InventoryCheck(ctx, order.Items)
    if err != nil {
        return err
    }
    // If the payment service times out, 30 seconds have already been wasted; the user has long since given up
    err = Charge(ctx, order.Total)
    if err != nil {
        return err
    }
    // Notification service also hangs... goroutine leak reaches the limit → OOM
    return Notify(ctx, order.UserID)
}

(2) 「Go」の解決策:背景

GO
// context_demo.go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // Root Context
    root := context.Background()

    // Wrap with WithTimeout: 2-second timeout
    ctx, cancel := context.WithTimeout(root, 2*time.Second)
    defer cancel() // Ensure resources are released

    result := PlaceOrder(ctx, "order-123")
    fmt.Println(result)
}

func PlaceOrder(ctx context.Context, orderID string) string {
    // Check if Context is already canceled
    select {
    case <-ctx.Done():
        return fmt.Sprintf("Canceled: %v", ctx.Err())
    default:
    }

    // Assign a shorter timeout to each downstream call
    checkCtx, cancel := context.WithTimeout(ctx, 1*time.Second)
    defer cancel()

    ok := InventoryCheck(checkCtx, orderID)
    if !ok {
        return "Insufficient inventory"
    }
    return "Order placed successfully"
}

func InventoryCheck(ctx context.Context, orderID string) bool {
    // Simulate a slow call
    select {
    case <-time.After(500 * time.Millisecond):
        return true
    case <-ctx.Done():
        fmt.Printf("InventoryCheck canceled: %v\n", ctx.Err())
        return false
    }
}

出力(通常):

TEXT 📖 参照専用
Order placed successfully

(3) メリット:文脈がある場合とない場合

状況 結果
タイムアウト制御なし ゴルーチンリーク、システムのメモリ不足 (OOM)
手動による時間設定。チェック後 コードが散在しており、各関数に独自のタイムアウトが設定されている
統合コンテキスト制御 親コンテキストがキャンセルされると、すべての子コンテキストがカスケード方式でキャンセルされます


3. ルートコンテキスト

(1) 背景とTODO

GO
package main

import (
    "context"
    "fmt"
)

func main() {
    // Background(): root Context, never canceled
    // Used in main functions, initialization, top-level requests
    ctx := context.Background()
    fmt.Printf("Background: %v\n", ctx)

    // TODO(): placeholder when unsure which Context to use
    // Marks code that has not yet been integrated with Context and needs refactoring
    todo := context.TODO()
    fmt.Printf("TODO: %v\n", todo)
}

(2) 背景 vs TODO

背景 目的 中止されるのか?
Background() ルートノード、すべてのコンテキストの起点 なし
TODO() コードがContextにまだ統合されていないことを示すプレースホルダー なし
💡 ヒント: context.Background() はすべてのコンテキストツリーのルートノードであり、決してキャンセルされることはありません。context.TODO() は、まだコンテキストチェーンに統合されていないコードをマークするために使用されます。できるだけ早く、適切なコンテキストに置き換えてください。



4. context.WithCancel: 手動によるキャンセル

▶ サンプル:ゴルーチンを手動でキャンセルする

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func Worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go Worker(ctx, 1)
    go Worker(ctx, 2)

    time.Sleep(2 * time.Second)
    fmt.Println("Main goroutine initiating cancellation...")
    cancel() // Tell all Workers to stop

    // Wait for goroutines to exit
    time.Sleep(500 * time.Millisecond)
}
▶ 試してみよう

▶ サンプル:連鎖的な取り消し

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func handler(ctx context.Context) {
    // Child Context inherits parent Context
    childCtx, cancel := context.WithCancel(ctx)
    defer cancel()

    go subTask(childCtx, "task-1")
    go subTask(childCtx, "task-2")

    // Parent Context canceled → Child Context automatically canceled
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("Handler complete")
    case <-ctx.Done():
        fmt.Println("Handler canceled")
    }
}

func subTask(ctx context.Context, name string) {
    select {
    case <-time.After(3 * time.Second):
        fmt.Printf("%s complete\n", name)
    case <-ctx.Done():
        fmt.Printf("%s canceled: %v\n", name, ctx.Err())
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go handler(ctx)

    time.Sleep(500 * time.Millisecond)
    cancel() // Cancel → handler → subTask all cascadingly canceled

    time.Sleep(1 * time.Second)
}
▶ 試してみよう
100%
sequenceDiagram
    participant Main as main()
    participant H as handler
    participant ST1 as subTask-1
    participant ST2 as subTask-2

    Main->>H: WithCancel
    H->>ST1: WithCancel
    H->>ST2: WithCancel
    Note over Main,ST2: Normal execution
    Main->>Main: cancel()
    Main-->>H: ctx.Done()
    H-->>ST1: ctx.Done()
    H-->>ST2: ctx.Done()
    Note over Main,ST2: All cascadingly canceled
🔥 よくある間違い: cancel() を呼び出す必要があります。WithTimeout を使用する場合でも、cancel() を遅延実行する必要があります。そうしないと、コンテキストのリソース(タイマー、ゴルーチン)が解放されません。ルール:WithCancelWithTimeout、またはWithDeadlineを作成した場合は、直ちにdefer cancel()を実行すること。



5. context.WithTimeout: タイムアウト時の自動キャンセル

▶ サンプル:タイムアウト制御

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func callExternalAPI(ctx context.Context, name string, delay time.Duration) (string, error) {
    select {
    case <-time.After(delay):
        return fmt.Sprintf("%s response", name), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // 1-second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // Call two downstream services
    result1 := make(chan string, 1)
    result2 := make(chan string, 1)

    go func() {
        r, err := callExternalAPI(ctx, "ServiceA", 500*time.Millisecond)
        if err != nil {
            result1 <- fmt.Sprintf("ServiceA failed: %v", err)
            return
        }
        result1 <- r
    }()

    go func() {
        r, err := callExternalAPI(ctx, "ServiceB", 1500*time.Millisecond)
        if err != nil {
            result2 <- fmt.Sprintf("ServiceB failed: %v", err)
            return
        }
        result2 <- r
    }()

    fmt.Println(<-result1) // ServiceA response (500ms < 1s timeout)
    fmt.Println(<-result2) // ServiceB failed: context deadline exceeded (1500ms > 1s)
}
▶ 試してみよう

▶ サンプル:WithTimeout 対 WithDeadline

GO
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    // WithTimeout: timeout 2 seconds from now
    timeoutCtx, cancel1 := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel1()

    // WithDeadline: specify absolute time
    deadline := time.Now().Add(2 * time.Second)
    deadlineCtx, cancel2 := context.WithDeadline(context.Background(), deadline)
    defer cancel2()

    // Both have the same effect
    fmt.Printf("timeoutCtx deadline: %v\n", timeoutCtx.Deadline())
    fmt.Printf("deadlineCtx deadline: %v\n", deadlineCtx.Deadline())
}
▶ 試してみよう

(3) WithTimeout 対 WithDeadline

メソッド パラメータ 目的
WithTimeout(parent, 2*time.Second) 相対時間 最もよく使われる表現:「最大2秒待つ」
WithDeadline(parent, time.Time) 絶対時間 締切を指定します:「15:30までに完了」


6. context.WithValue を使用したリクエストレベルでの値の渡し方

▶ サンプル:WithValue

GO
package main

import (
    "context"
    "fmt"
)

// Custom key type (avoids conflicts)
type contextKey string

const (
    UserIDKey    contextKey = "user_id"
    TraceIDKey   contextKey = "trace_id"
    RequestIDKey contextKey = "request_id"
)

func middleware(ctx context.Context) context.Context {
    // Get trace ID from request header
    ctx = context.WithValue(ctx, TraceIDKey, "trace-123")
    ctx = context.WithValue(ctx, RequestIDKey, "req-456")
    return ctx
}

func handler(ctx context.Context, userID string) {
    ctx = context.WithValue(ctx, UserIDKey, userID)

    // Pass to business layer
    service(ctx)
}

func service(ctx context.Context) {
    // Retrieve values from Context
    userID := ctx.Value(UserIDKey).(string)
    traceID := ctx.Value(TraceIDKey).(string)
    requestID := ctx.Value(RequestIDKey).(string)

    fmt.Printf("Processing request: user=%s, trace=%s, request=%s\n",
        userID, traceID, requestID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx)
    handler(ctx, "user-007")
}
▶ 試してみよう
🔥 よくある間違い: context.WithValue 内の key はカスタム型でなければなりません。文字列を直接使用することはできません。2つのパッケージがともに "user_id" という文字列をキーとして使用すると、競合が発生します。カスタム型 type contextKey string を使用することがベストプラクティスです。

(2) WithValue のユースケース

シナリオ 推奨 非推奨
TraceID / RequestID ✅ WithValue 経由で渡された ❌ グローバル変数
認証トークン ✅ WithValue によって渡された ❌ 関数のパラメータ
データベース接続 ❌ 依存性注入により取得 ❌ WithValue
ビジネスパラメータ ❌ 明示的パラメータ ❌ WithValue による暗黙的渡し


7. コンテキスト・チェイニングのルール

GO
package main

import (
    "context"
    "fmt"
    "time"
)

type contextKey string

func main() {
    root := context.Background()

    // Chaining: WithCancel → WithTimeout → WithValue
    ctx1, cancel1 := context.WithCancel(root)
    defer cancel1()

    ctx2, cancel2 := context.WithTimeout(ctx1, 2*time.Second)
    defer cancel2()

    ctx3 := context.WithValue(ctx2, contextKey("trace"), "trace-007")

    // ctx3 inherits ctx1's cancellation + ctx2's timeout + ctx3's value
    fmt.Printf("ctx3 deadline: %v\n", ctx3.Deadline())
    fmt.Printf("ctx3 value: %v\n", ctx3.Value(contextKey("trace")))

    // Cancel ctx1 first → ctx2 and ctx3 both receive the cancellation signal
    cancel1()
    time.Sleep(10 * time.Millisecond)
    fmt.Printf("ctx2 err: %v\n", ctx2.Err())
    fmt.Printf("ctx3 err: %v\n", ctx3.Err())
}

(1) コンテキストの引き継ぎに関するルール

ルール 説明
最初の引数 関数のシグネチャにおける最初の引数は常に ctx context.Context です
構造体内に格納しないでください Contextを構造体のフィールドに格納せず、パラメータとして渡してください
関数間の引き渡し キャンセルやタイムアウトを認識する必要がある各関数には、コンテキストが渡されます
不変チェーン WithCancel/WithTimeout/WithValue の呼び出しごとに新しいコンテキストが返される
連鎖的なキャンセル 親アイテムをキャンセルすると → すべての子アイテムがキャンセルされます。子アイテムをキャンセルしても、親アイテムには影響しません


8. 完全な例:マイクロサービスのトレースにおけるタイムアウト制御

GO
// microservice_chain.go
package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

// ---------- Simulated downstream services ----------

// Inventory service
type InventoryService struct{}

func (s *InventoryService) Check(ctx context.Context, orderID string) (bool, error) {
    // Simulate random delay
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return true, nil
    case <-ctx.Done():
        return false, ctx.Err()
    }
}

// Payment service
type PaymentService struct{}

func (s *PaymentService) Charge(ctx context.Context, amount float64) (string, error) {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return "pay-" + fmt.Sprintf("%d", time.Now().UnixNano()), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

// Notification service
type NotificationService struct{}

func (s *NotificationService) Send(ctx context.Context, userID, message string) error {
    delay := time.Duration(rand.Intn(1500)) * time.Millisecond
    select {
    case <-time.After(delay):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

// ---------- Business layer ----------

type OrderService struct {
    inventory *InventoryService
    payment   *PaymentService
    notify    *NotificationService
}

func NewOrderService() *OrderService {
    return &OrderService{
        inventory: &InventoryService{},
        payment:   &PaymentService{},
        notify:    &NotificationService{},
    }
}

// PlaceOrder uses Context to control the entire chain's timeout
func (s *OrderService) PlaceOrder(ctx context.Context, userID, orderID string, amount float64) error {
    // 1. Inventory check (wait at most 1 second)
    invCtx, invCancel := context.WithTimeout(ctx, 1*time.Second)
    defer invCancel()

    ok, err := s.inventory.Check(invCtx, orderID)
    if err != nil {
        return fmt.Errorf("inventory check failed: %w", err)
    }
    if !ok {
        return fmt.Errorf("insufficient inventory")
    }

    // 2. Payment (wait at most 2 seconds)
    payCtx, payCancel := context.WithTimeout(ctx, 2*time.Second)
    defer payCancel()

    paymentID, err := s.payment.Charge(payCtx, amount)
    if err != nil {
        return fmt.Errorf("payment failed: %w", err)
    }

    // 3. Notification (wait at most 500ms)
    notifyCtx, notifyCancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer notifyCancel()

    err = s.notify.Send(notifyCtx, userID, "Order placed: "+orderID)
    if err != nil {
        // Notification failure does not affect the order (async log recording)
        fmt.Printf("Notification failed (logged): %v\n", err)
    }

    fmt.Printf("Order placed successfully: user=%s, order=%s, payment=%s\n", userID, orderID, paymentID)
    return nil
}

// ---------- Client ----------

func main() {
    svc := NewOrderService()

    // Overall request timeout of 3 seconds
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := svc.PlaceOrder(ctx, "user-007", "order-123", 99.99)
    if err != nil {
        fmt.Printf("Order failed: %v\n", err)
    }
}
💡 ヒント: Err()Context メソッドは、context.Canceled(手動でキャンセルされた)または context.DeadlineExceeded(タイムアウト)の 2 つの値のいずれかを返します。チェーン内では、errors.Is(err, context.DeadlineExceeded) を使用して、どのタイプのキャンセルが発生したかを特定し、それに応じて再試行戦略を決定することができます。


❓ よくある質問

Q Context はどのような場合に使用すべきですか?
A タイムアウト制御、手動によるキャンセル、またはリクエストレベルでの値の受け渡しが必要なあらゆるシナリオで使用します。これには、HTTP リクエストの処理、データベースクエリ、RPC 呼び出し、およびスケジュールされたタスクが含まれます。キャンセルやタイムアウト制御を必要としない純粋な計算関数では、Context を使用しないでください。
Q Background()TODO() の違いは何ですか?
A どちらもキャンセルできないルートコンテキストですが、意味が異なります。Background() は、コード内で現在アクティブに使用しているルートです。TODO()はプレースホルダーであり、このコードセクションがまだコンテキストに統合されておらず、できるだけ早くリファクタリングする必要があることを示しています。
Q WithTimeoutWithDeadline、どちらを選べばいいですか?
A ほとんどの場合は WithTimeout を使用してください(「最大 2 秒待つ」という表現の方が直感的です)。タイムアウトが絶対時間(「15:30:00 までに完了する」)である場合にのみ、WithDeadline を使用してください。WithTimeoutは内部でWithDeadlineを呼び出します。
Q Context は複数回キャンセルできますか?
A キャンセルは 1 回のみ発生します。cancel() を繰り返し呼び出しても問題ありません(2 回目以降の呼び出しは何も行われません)。Done() をリッスンしているすべての goroutine は、このシグナルを 1 回だけ受信します。
Q 親のキャンセルとタイムアウトが同時に発生した場合はどうなりますか?
A これらは独立してトリガーされ、先に発生した方が適用されます。親のコンテキストが手動でキャンセルされた場合、子コンテキストのタイムアウトがまだ切れていなくても、すべての子コンテキストは直ちにDone()シグナルを受け取ります。これにより、キャンセルの連鎖が最短経路で進行することが保証されます。
Q WithValue に渡された値は変更できますか?
A いいえ。context.WithValue の値は不変です。「変更」と呼ばれる操作は、本質的には新しいコンテキストを作成することに相当します。子コンテキストの WithValue は、親コンテキストには影響を与えません。これにより、並行処理の安全性が確保されます。
Q Context にはどのような種類のデータを格納するのに適していますか?
A リクエストレベルのメタデータ(TraceID、RequestID、UserID、認証トークンなど)の格納にのみ適しています。ビジネスパラメータ(価格や数量など)の格納には適しておらず、データベース接続や設定情報の格納にも適していません。これらは依存性注入を介して渡す必要があります。

📖 まとめ


📝 練習問題

  1. 基本演習(難易度 ⭐)context.WithTimeout を使用して HTTP リクエストのタイムアウトを制御する関数 FetchWithTimeout(ctx, url string, timeout time.Duration) を作成してください。タイムアウトが切れたら、その HTTP リクエストは自動的にキャンセルされるようにしてください。

  2. 上級問題(難易度 ⭐⭐)並行してキャンセル可能なスケジュールタスクマネージャーを実装してください。以下の機能をサポートする必要があります:(1) 複数のスケジュールタスクの登録(1つのgoroutineにつき1つ);(2) 個別のキャンセル(cancel関数の呼び出しによる);(3) 一括キャンセル(WithCancelの連鎖); (4) すべてのタスクが単一のルートコンテキストを共有すること。

  3. 課題(難易度 ⭐⭐⭐)分散トレーシングシステムをシミュレートしてください。要件:(1) WithValue を使用して、3 層の関数呼び出し(API → サービス → DB)にわたって TraceID を渡すこと; (2) 各層には独自のタイムアウト制御がある(Service:2秒、DB:500ミリ秒);(3) タイムアウトが発生した場合、現在の層はキャンセルされるが、上流の層には影響を与えない;(4) 各ステージの経過時間とTraceIDを出力する。ContextDeadline()を使用して残り時間を計算する。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%