Go: Goのコンテキスト:WithCancel、WithTimeout、WithDeadline、WithValue、チ…
最終更新:2026-08-26
コンテキストは、Goにおける並行処理制御の要です。これにより、タイムアウト、キャンセル、および値が、goroutineの呼び出しチェーンを通じて円滑に渡されるようになります。
サービスが数十もの外部依存関係を同時に処理する必要がある場合(それぞれに独自のタイムアウトやキャンセルロジックがある)、それらを一貫性のある方法で管理するにはどうすればよいでしょうか?このレッスンでは、Goのcontextパッケージの包括的な活用方法を学びます。
1. 学習内容
context.Background()およびcontext.TODO()context.WithCancel手動によるキャンセルcontext.WithTimeoutはタイムアウト後に自動的にキャンセルされますcontext.WithDeadline締切の取り消しcontext.WithValue: リクエストレベルでの値の受け渡し- 背景:チェーンパスのルール
- 実践ガイド:マイクロサービスチェーンにおけるタイムアウトの連鎖を防ぐ
2. バックエンドエンジニアの実話
(1) 課題:上流のコンポーネントの1つでタイムアウトが発生すると、システム全体がクラッシュしてしまう
Xiaoliは注文システムのエンジニアです。彼女は、以下の3つの下流サービスに依存するREST APIを担当しています:
「注文処理APIは、在庫サービス、決済サービス、通知サービスの3つのサービスを呼び出していました。ある日、在庫サービスの応答に20秒かかってしまい、すべてのgoroutineがその応答を待っていたため、私のサービスのメモリ使用量が急増しました。その結果、他のユーザーからのリクエストも処理できなくなってしまいました。上司から『注文ページが完全にダウンしているのはなぜだ?』と尋ねられました。」
問題の分析:
// 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」の解決策:背景
// 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
}
}
出力(通常):
Order placed successfully
(3) メリット:文脈がある場合とない場合
| 状況 | 結果 |
|---|---|
| タイムアウト制御なし | ゴルーチンリーク、システムのメモリ不足 (OOM) |
| 手動による時間設定。チェック後 | コードが散在しており、各関数に独自のタイムアウトが設定されている |
| 統合コンテキスト制御 | 親コンテキストがキャンセルされると、すべての子コンテキストがカスケード方式でキャンセルされます |
3. ルートコンテキスト
(1) 背景とTODO
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: 手動によるキャンセル
▶ サンプル:ゴルーチンを手動でキャンセルする
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)
}
▶ サンプル:連鎖的な取り消し
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)
}
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() を遅延実行する必要があります。そうしないと、コンテキストのリソース(タイマー、ゴルーチン)が解放されません。ルール:WithCancel、WithTimeout、またはWithDeadlineを作成した場合は、直ちにdefer cancel()を実行すること。
5. context.WithTimeout: タイムアウト時の自動キャンセル
▶ サンプル:タイムアウト制御
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
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
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. コンテキスト・チェイニングのルール
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. 完全な例:マイクロサービスのトレースにおけるタイムアウト制御
// 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) を使用して、どのタイプのキャンセルが発生したかを特定し、それに応じて再試行戦略を決定することができます。
❓ よくある質問
Background() と TODO() の違いは何ですか?Background() は、コード内で現在アクティブに使用しているルートです。TODO()はプレースホルダーであり、このコードセクションがまだコンテキストに統合されておらず、できるだけ早くリファクタリングする必要があることを示しています。WithTimeout と WithDeadline、どちらを選べばいいですか?WithTimeout を使用してください(「最大 2 秒待つ」という表現の方が直感的です)。タイムアウトが絶対時間(「15:30:00 までに完了する」)である場合にのみ、WithDeadline を使用してください。WithTimeoutは内部でWithDeadlineを呼び出します。Context は複数回キャンセルできますか?cancel() を繰り返し呼び出しても問題ありません(2 回目以降の呼び出しは何も行われません)。Done() をリッスンしているすべての goroutine は、このシグナルを 1 回だけ受信します。WithValue に渡された値は変更できますか?context.WithValue の値は不変です。「変更」と呼ばれる操作は、本質的には新しいコンテキストを作成することに相当します。子コンテキストの WithValue は、親コンテキストには影響を与えません。これにより、並行処理の安全性が確保されます。📖 まとめ
- Background() / TODO(): 2種類のルートコンテキスト。キャンセルすることはできない
- WithCancel: ゴルーチンの中止を手動で制御する
- WithTimeout / WithDeadline:タイムアウト時に自動的にキャンセルされる
- WithValue:リクエストレベルでの値の渡し(カスタムキー型)
- チェーンパッシング:最初の引数は常にContextです
- カスケードキャンセル:親項目のキャンセル → すべての子項目がキャンセルされる
cancel()はdeferを使用して呼び出す必要があります- 実践ガイド:マイクロサービスチェーンにおけるタイムアウトの連鎖を防ぐ
📝 練習問題
-
基本演習(難易度 ⭐):
context.WithTimeoutを使用して HTTP リクエストのタイムアウトを制御する関数FetchWithTimeout(ctx, url string, timeout time.Duration)を作成してください。タイムアウトが切れたら、その HTTP リクエストは自動的にキャンセルされるようにしてください。 -
上級問題(難易度 ⭐⭐):並行してキャンセル可能なスケジュールタスクマネージャーを実装してください。以下の機能をサポートする必要があります:(1) 複数のスケジュールタスクの登録(1つのgoroutineにつき1つ);(2) 個別のキャンセル(
cancel関数の呼び出しによる);(3) 一括キャンセル(WithCancelの連鎖); (4) すべてのタスクが単一のルートコンテキストを共有すること。 -
課題(難易度 ⭐⭐⭐):分散トレーシングシステムをシミュレートしてください。要件:(1)
WithValueを使用して、3 層の関数呼び出し(API → サービス → DB)にわたって TraceID を渡すこと; (2) 各層には独自のタイムアウト制御がある(Service:2秒、DB:500ミリ秒);(3) タイムアウトが発生した場合、現在の層はキャンセルされるが、上流の層には影響を与えない;(4) 各ステージの経過時間とTraceIDを出力する。ContextのDeadline()を使用して残り時間を計算する。