第19章: コンテキスト

はじめに

contextパッケージは、Go 1.7で標準ライブラリに追加された重要な機能です。goroutine間でキャンセルシグナル、タイムアウト、リクエストスコープの値を伝播させるための仕組みを提供します。特に、HTTPサーバーやデータベース操作など、長時間実行される処理の制御に不可欠です。この章では、contextの内部実装を深く理解し、実践的な使用方法を学びます。

Contextインターフェースの内部構造

基本インターフェース

type Context interface {
    Deadline() (deadline time.Time, ok bool)  // デッドラインを返す
    Done() <-chan struct{}                    // キャンセルチャネル
    Err() error                               // キャンセル理由
    Value(key interface{}) interface{}        // 値の取得
}

🔑 重要: このシンプルな4つのメソッドだけで、複雑な並行処理の制御を実現します。

実装の階層構造

context.Context (interface)
        │
        ├─ emptyCtx (Background, TODO)
        │      │
        │      └─ 空の実装(キャンセル不可)
        │
        ├─ cancelCtx (WithCancel)
        │      │
        │      ├─ done chan struct{}
        │      ├─ children map[canceler]struct{}
        │      └─ err error
        │
        ├─ timerCtx (WithTimeout, WithDeadline)
        │      │
        │      ├─ cancelCtx (埋め込み)
        │      ├─ timer *time.Timer
        │      └─ deadline time.Time
        │
        └─ valueCtx (WithValue)
               │
               ├─ Context (親)
               ├─ key interface{}
               └─ val interface{}

emptyCtxの実装

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return  // ゼロ値を返す(デッドラインなし)
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil  // nilチャネル(永遠にブロック)
}

func (*emptyCtx) Err() error {
    return nil  // エラーなし
}

func (*emptyCtx) Value(key interface{}) interface{} {
    return nil  // 値なし
}

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

func Background() Context { return background }
func TODO() Context       { return todo }

💡 設計の理由: Background()TODO()は実際には同じ型ですが、意味的に異なる用途を示すために分けられています。

キャンセル処理の内部実装

cancelCtxの構造

type cancelCtx struct {
    Context  // 親コンテキスト

    mu       sync.Mutex               // 以下のフィールドを保護
    done     chan struct{}            // 初回キャンセル時にクローズ
    children map[canceler]struct{}    // 子コンテキストの集合
    err      error                    // キャンセル理由
}

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

WithCancelの内部動作

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

propagateCancel関数の重要性:

親コンテキスト
    │
    v
┌──────────────────────────────────────┐
│ 親がキャンセル可能?                   │
├──────────────────────────────────────┤
│ YES → 親のchildrenマップに追加        │
│       親キャンセル時に自動キャンセル   │
│                                      │
│ NO  → goroutineで親のDone監視         │
│       親キャンセル時に伝播            │
└──────────────────────────────────────┘

キャンセル伝播の仕組み

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }

    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return  // 既にキャンセル済み
    }

    c.err = err

    if c.done == nil {
        c.done = closedchan  // 事前にクローズされたチャネル
    } else {
        close(c.done)  // Doneチャネルをクローズ
    }

    // 全ての子をキャンセル
    for child := range c.children {
        child.cancel(false, err)  // 再帰的にキャンセル
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

キャンセル伝播の視覚化:

親コンテキスト
    │
    ├─ 子1 (cancelCtx)
    │   ├─ 孫1
    │   └─ 孫2
    │
    └─ 子2 (cancelCtx)
        └─ 孫3

親.cancel() 呼び出し
    │
    ├─► 子1.cancel()
    │    ├─► 孫1.cancel()
    │    └─► 孫2.cancel()
    │
    └─► 子2.cancel()
         └─► 孫3.cancel()

全て同時にDoneチャネルがクローズ

実践例:並行Worker制御

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()

    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: 停止 (理由: %v)\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d: 処理中...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    // ルートコンテキスト
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup

    // 5つのworkerを起動
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }

    // 3秒後に全てキャンセル
    time.Sleep(3 * time.Second)
    fmt.Println("\n全workerをキャンセル中...")
    cancel()  // ← 1回の呼び出しで全てのworkerが停止

    wg.Wait()
    fmt.Println("全worker停止完了")
}

🔑 重要: cancel()は何度呼んでも安全です(冪等性)。2回目以降は何もしません。

タイムアウト処理の深層

timerCtxの実装

type timerCtx struct {
    cancelCtx          // cancelCtxを埋め込み
    timer     *time.Timer
    deadline  time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()  // タイマーを停止
        c.timer = nil
    }
    c.mu.Unlock()
}

WithTimeoutの内部フロー

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // 親のデッドラインの方が早い → 親と同じ
        return WithCancel(parent)
    }

    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)

    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded)  // 既に期限切れ
        return c, func() { c.cancel(false, Canceled) }
    }

    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

タイムアウトの動作フロー:

WithTimeout(parent, 5*time.Second)
    │
    v
┌──────────────────────────────────┐
│ time.Timer を 5秒後に設定         │
└────────┬─────────────────────────┘
         │
         ├─► 5秒以内に手動cancel()
         │   → timer.Stop()
         │   → err = context.Canceled
         │
         └─► 5秒経過
             → timer発火
             → c.cancel(true, DeadlineExceeded)
             → err = context.DeadlineExceeded

親子関係のデッドライン継承

package main

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

func main() {
    // 親: 10秒のタイムアウト
    parentCtx, parentCancel := context.WithTimeout(
        context.Background(),
        10*time.Second,
    )
    defer parentCancel()

    // 子1: 3秒のタイムアウト(親より早い)
    child1Ctx, child1Cancel := context.WithTimeout(parentCtx, 3*time.Second)
    defer child1Cancel()

    // 子2: 15秒のタイムアウト(親より遅い → 実質10秒)
    child2Ctx, child2Cancel := context.WithTimeout(parentCtx, 15*time.Second)
    defer child2Cancel()

    // 各コンテキストのデッドラインを確認
    printDeadline := func(name string, ctx context.Context) {
        if deadline, ok := ctx.Deadline(); ok {
            fmt.Printf("%s のデッドライン: %v (残り %v)\n",
                name,
                deadline.Format("15:04:05"),
                time.Until(deadline),
            )
        }
    }

    printDeadline("親", parentCtx)
    printDeadline("子1", child1Ctx)
    printDeadline("子2", child2Ctx)

    // 実験: 子1は3秒で、子2は10秒でタイムアウト
    go func() {
        <-child1Ctx.Done()
        fmt.Printf("子1 タイムアウト: %v\n", child1Ctx.Err())
    }()

    go func() {
        <-child2Ctx.Done()
        fmt.Printf("子2 タイムアウト: %v\n", child2Ctx.Err())
    }()

    time.Sleep(12 * time.Second)
}

💡 デッドライン継承のルール: 子コンテキストは親よりも長いデッドラインを設定できません。親が先にキャンセルされるため、実質的に親のデッドラインが適用されます。

データベース操作とコンテキスト

クエリタイムアウトの実装

package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

type User struct {
    ID   int
    Name string
}

func queryUsersWithTimeout(db *sql.DB, timeout time.Duration) ([]User, error) {
    // タイムアウト付きコンテキスト
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    // コンテキストを渡してクエリ実行
    rows, err := db.QueryContext(ctx, "SELECT id, name FROM users")
    if err != nil {
        return nil, fmt.Errorf("クエリ失敗: %w", err)
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.Name); err != nil {
            return nil, fmt.Errorf("スキャン失敗: %w", err)
        }
        users = append(users, user)
    }

    // タイムアウトチェック
    if err := ctx.Err(); err != nil {
        return nil, fmt.Errorf("タイムアウト: %w", err)
    }

    return users, nil
}

QueryContextの内部動作:

db.QueryContext(ctx, query)
    │
    v
┌──────────────────────────────────────┐
│ 1. クエリをDBに送信                   │
└────────┬─────────────────────────────┘
         │
         v
┌──────────────────────────────────────┐
│ 2. goroutineで結果待機                │
│    select {                          │
│      case <-ctx.Done():              │
│        → クエリキャンセル             │
│        → KILL QUERY 送信 (MySQL)     │
│      case result := <-resultChan:    │
│        → 正常完了                     │
│    }                                 │
└──────────────────────────────────────┘

トランザクションのタイムアウト

func transferWithTimeout(db *sql.DB, fromID, toID int, amount float64) error {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // トランザクション開始
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()  // エラー時は自動ロールバック

    // 送金元から引き落とし
    _, err = tx.ExecContext(ctx,
        "UPDATE accounts SET balance = balance - ? WHERE id = ?",
        amount, fromID,
    )
    if err != nil {
        return fmt.Errorf("引き落とし失敗: %w", err)
    }

    // 重い処理をシミュレート
    time.Sleep(3 * time.Second)

    // コンテキストチェック
    if err := ctx.Err(); err != nil {
        return fmt.Errorf("タイムアウト: %w", err)
    }

    // 送金先に入金
    _, err = tx.ExecContext(ctx,
        "UPDATE accounts SET balance = balance + ? WHERE id = ?",
        amount, toID,
    )
    if err != nil {
        return fmt.Errorf("入金失敗: %w", err)
    }

    // コミット
    return tx.Commit()
}

🔑 重要: トランザクション中にタイムアウトすると、自動的にロールバックされます。データの一貫性が保たれます。

HTTPサーバーとコンテキスト

リクエストコンテキストのライフサイクル

HTTP リクエスト受信
    │
    v
┌──────────────────────────────────────┐
│ net/http が自動でコンテキスト作成     │
│ - 親: Background                     │
│ - キャンセル条件:                    │
│   1. クライアント接続切断             │
│   2. ServeHTTP完了                   │
│   3. http.Server.Shutdown            │
└────────┬─────────────────────────────┘
         │
         v
┌──────────────────────────────────────┐
│ r.Context() でアクセス可能            │
│ - ハンドラー内で使用                  │
│ - 子コンテキスト作成可能              │
└────────┬─────────────────────────────┘
         │
         v
┌──────────────────────────────────────┐
│ ServeHTTP 終了時に自動キャンセル      │
└──────────────────────────────────────┘

ハンドラーでのタイムアウト実装

func longRunningHandler(w http.ResponseWriter, r *http.Request) {
    // リクエストコンテキストから派生
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    // 結果チャネル
    result := make(chan string, 1)
    errChan := make(chan error, 1)

    // 重い処理を別goroutineで実行
    go func() {
        // データベースクエリ(時間かかる)
        data, err := fetchDataFromDB(ctx)
        if err != nil {
            errChan <- err
            return
        }

        // 外部API呼び出し(時間かかる)
        processed, err := processWithExternalAPI(ctx, data)
        if err != nil {
            errChan <- err
            return
        }

        result <- processed
    }()

    // タイムアウトまたは完了を待機
    select {
    case res := <-result:
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "status": "success",
            "data":   res,
        })

    case err := <-errChan:
        http.Error(w, err.Error(), http.StatusInternalServerError)

    case <-ctx.Done():
        // タイムアウトまたはクライアント切断
        http.Error(w, "Request timeout", http.StatusRequestTimeout)
        fmt.Printf("リクエストキャンセル: %v\n", ctx.Err())
    }
}

クライアント接続切断の検知

func streamingHandler(w http.ResponseWriter, r *http.Request) {
    flusher, ok := w.(http.Flusher)
    if !ok {
        http.Error(w, "Streaming not supported", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/event-stream")
    w.Header().Set("Cache-Control", "no-cache")
    w.Header().Set("Connection", "keep-alive")

    ctx := r.Context()

    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            // クライアントが接続を切った
            fmt.Println("クライアント切断:", ctx.Err())
            return

        case t := <-ticker.C:
            // データを送信
            fmt.Fprintf(w, "data: Current time: %s\n\n", t.Format("15:04:05"))
            flusher.Flush()
        }
    }
}

💡 ストリーミングレスポンス: サーバーサイドイベント(SSE)やロングポーリングでは、クライアント切断を検知して適切にリソースを解放することが重要です。

値の伝播(WithValue)

valueCtxの内部実装

type valueCtx struct {
    Context      // 親コンテキスト
    key, val interface{}
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)  // 親に委譲
}

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

値の検索プロセス(連結リスト構造):

Value(key3) を検索

valueCtx(key1=val1)
    │
    ├─► key == key1? NO
    │   → 親に委譲
    v
valueCtx(key2=val2)
    │
    ├─► key == key2? NO
    │   → 親に委譲
    v
valueCtx(key3=val3)
    │
    └─► key == key3? YES ← 発見!
        → return val3

時間計算量: O(n) (nは階層の深さ)

⚠️ パフォーマンス注意: 深い階層で頻繁にValue()を呼ぶとパフォーマンスが低下します。リクエストスコープの少数の値のみに使用してください。

型安全なキー設計

package main

import (
    "context"
    "fmt"
)

// キー型を独自定義(衝突防止)
type contextKey string

const (
    userIDKey    contextKey = "userID"
    requestIDKey contextKey = "requestID"
    traceIDKey   contextKey = "traceID"
)

// ヘルパー関数で型安全に
func WithUserID(ctx context.Context, userID string) context.Context {
    return context.WithValue(ctx, userIDKey, userID)
}

func GetUserID(ctx context.Context) (string, bool) {
    userID, ok := ctx.Value(userIDKey).(string)
    return userID, ok
}

func WithRequestID(ctx context.Context, requestID string) context.Context {
    return context.WithValue(ctx, requestIDKey, requestID)
}

func GetRequestID(ctx context.Context) (string, bool) {
    requestID, ok := ctx.Value(requestIDKey).(string)
    return requestID, ok
}

// 使用例
func handleRequest(ctx context.Context) {
    // 値を追加
    ctx = WithUserID(ctx, "user123")
    ctx = WithRequestID(ctx, "req456")

    // 別の関数に渡す
    processData(ctx)
}

func processData(ctx context.Context) {
    userID, ok := GetUserID(ctx)
    if !ok {
        fmt.Println("ユーザーID が見つかりません")
        return
    }

    requestID, ok := GetRequestID(ctx)
    if !ok {
        fmt.Println("リクエストID が見つかりません")
        return
    }

    fmt.Printf("処理中: ユーザー=%s リクエスト=%s\n", userID, requestID)
}

func main() {
    ctx := context.Background()
    handleRequest(ctx)
}

🔑 ベストプラクティス: 独自の型を定義することで、他のパッケージとのキー衝突を防ぎます。

構造体を使った複雑な値の伝播

package main

import (
    "context"
    "fmt"
)

type contextKey int

const requestMetadataKey contextKey = 0

// リクエストメタデータ
type RequestMetadata struct {
    UserID      string
    RequestID   string
    IPAddress   string
    UserAgent   string
    StartTime   time.Time
}

func WithRequestMetadata(ctx context.Context, meta *RequestMetadata) context.Context {
    return context.WithValue(ctx, requestMetadataKey, meta)
}

func GetRequestMetadata(ctx context.Context) (*RequestMetadata, bool) {
    meta, ok := ctx.Value(requestMetadataKey).(*RequestMetadata)
    return meta, ok
}

// ミドルウェアでメタデータを注入
func requestMetadataMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        meta := &RequestMetadata{
            UserID:      extractUserID(r),
            RequestID:   generateRequestID(),
            IPAddress:   r.RemoteAddr,
            UserAgent:   r.UserAgent(),
            StartTime:   time.Now(),
        }

        ctx := WithRequestMetadata(r.Context(), meta)
        r = r.WithContext(ctx)

        next(w, r)
    }
}

// ハンドラーでメタデータを使用
func handler(w http.ResponseWriter, r *http.Request) {
    meta, ok := GetRequestMetadata(r.Context())
    if !ok {
        http.Error(w, "メタデータなし", http.StatusInternalServerError)
        return
    }

    // ログ出力
    log.Printf(
        "ユーザー=%s リクエスト=%s IP=%s 処理時間=%v",
        meta.UserID,
        meta.RequestID,
        meta.IPAddress,
        time.Since(meta.StartTime),
    )

    fmt.Fprintf(w, "こんにちは、%s さん", meta.UserID)
}

実践的なパターン

パターン1: 階層的なタイムアウト

func orchestrateServices(ctx context.Context) error {
    // 全体で30秒のタイムアウト
    globalCtx, globalCancel := context.WithTimeout(ctx, 30*time.Second)
    defer globalCancel()

    // サービスA: 10秒
    serviceACtx, cancelA := context.WithTimeout(globalCtx, 10*time.Second)
    defer cancelA()

    resultA, err := callServiceA(serviceACtx)
    if err != nil {
        return fmt.Errorf("サービスA失敗: %w", err)
    }

    // サービスB: 15秒(サービスAの結果を使用)
    serviceBCtx, cancelB := context.WithTimeout(globalCtx, 15*time.Second)
    defer cancelB()

    resultB, err := callServiceB(serviceBCtx, resultA)
    if err != nil {
        return fmt.Errorf("サービスB失敗: %w", err)
    }

    // サービスC: 5秒(AとBの結果を統合)
    serviceCCtx, cancelC := context.WithTimeout(globalCtx, 5*time.Second)
    defer cancelC()

    return callServiceC(serviceCCtx, resultA, resultB)
}

タイムアウトの関係:

全体 (30秒)
├─ サービスA (10秒)
├─ サービスB (15秒)
└─ サービスC (5秒)

実際の制約:
- サービスA: 10秒 (個別)
- サービスB: min(15秒, 30秒-A実行時間)
- サービスC: min(5秒, 30秒-A実行時間-B実行時間)

パターン2: Fan-Out/Fan-In with Context

func parallelFetch(ctx context.Context, urls []string) ([]string, error) {
    resultsChan := make(chan string, len(urls))
    errorsChan := make(chan error, len(urls))

    // Fan-Out: 各URLを並列フェッチ
    for _, url := range urls {
        go func(u string) {
            data, err := fetchWithContext(ctx, u)
            if err != nil {
                errorsChan <- err
                return
            }
            resultsChan <- data
        }(url)
    }

    // Fan-In: 結果を収集
    var results []string
    for i := 0; i < len(urls); i++ {
        select {
        case result := <-resultsChan:
            results = append(results, result)

        case err := <-errorsChan:
            return nil, err

        case <-ctx.Done():
            return nil, ctx.Err()
        }
    }

    return results, nil
}

func fetchWithContext(ctx context.Context, url string) (string, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return "", err
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    data, err := io.ReadAll(resp.Body)
    return string(data), err
}

パターン3: 早期終了(First Win)

func fetchFastest(ctx context.Context, urls []string) (string, error) {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    resultChan := make(chan string, 1)
    errorChan := make(chan error, len(urls))

    for _, url := range urls {
        go func(u string) {
            data, err := fetchWithContext(ctx, u)
            if err != nil {
                errorChan <- err
                return
            }

            select {
            case resultChan <- data:
                // 最初の成功を送信
            case <-ctx.Done():
                // 既に他が成功済み
            }
        }(url)
    }

    select {
    case result := <-resultChan:
        // 最初に成功したものを返す
        cancel()  // 他のgoroutineをキャンセル
        return result, nil

    case <-ctx.Done():
        return "", ctx.Err()
    }
}

💡 最適化: 最初のレスポンスを受け取ったら、他のリクエストをすぐにキャンセルすることでリソースを節約します。

エラーハンドリング

context.Err()の使い分け

func processWithContext(ctx context.Context) error {
    result, err := longOperation(ctx)
    if err != nil {
        // コンテキストエラーかチェック
        if ctx.Err() == context.Canceled {
            return fmt.Errorf("処理がキャンセルされました")
        }
        if ctx.Err() == context.DeadlineExceeded {
            return fmt.Errorf("処理がタイムアウトしました")
        }
        return fmt.Errorf("処理エラー: %w", err)
    }
    return nil
}

エラーの種類:

var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}

func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }  ← net.Errorインターフェース
func (deadlineExceededError) Temporary() bool { return true }

ラップされたエラーの扱い

func robustOperation(ctx context.Context) error {
    err := someOperation(ctx)
    if err != nil {
        // errors.Isでチェック(ラップされていてもOK)
        if errors.Is(err, context.Canceled) {
            log.Println("ユーザーがキャンセルしました")
            return nil  // キャンセルは正常終了として扱う
        }

        if errors.Is(err, context.DeadlineExceeded) {
            log.Println("タイムアウトしました")
            return ErrTimeout
        }

        return fmt.Errorf("予期しないエラー: %w", err)
    }
    return nil
}

パフォーマンスとメモリ管理

コンテキストのメモリオーバーヘッド

emptyCtx:
    サイズ: 0バイト (型のみ)

cancelCtx:
    - Context: 8バイト (ポインタ)
    - mu: 8バイト (sync.Mutex)
    - done: 8バイト (chan struct{})
    - children: 8バイト (map)
    - err: 16バイト (interface{})
    合計: 約48バイト + childrenマップのサイズ

timerCtx:
    - cancelCtx: 48バイト
    - timer: 8バイト (ポインタ)
    - deadline: 24バイト (time.Time)
    合計: 約80バイト

valueCtx:
    - Context: 8バイト (ポインタ)
    - key: 16バイト (interface{})
    - val: 16バイト (interface{})
    合計: 約40バイト

過度なWithValueの問題

// 悪い例: 深いネスト
ctx := context.Background()
for i := 0; i < 1000; i++ {
    ctx = context.WithValue(ctx, fmt.Sprintf("key%d", i), i)
}
// 1000層の連結リスト → Value検索が遅い

// 良い例: 構造体にまとめる
type Metadata struct {
    Values map[string]interface{}
}

ctx := context.WithValue(context.Background(), metadataKey, &Metadata{
    Values: make(map[string]interface{}),
})
// 1層のみ → Value検索が高速

🔑 最適化: 複数の値を伝播する必要がある場合は、構造体にまとめて1つのWithValueで設定します。

自己診断問題

以下の問題に答えて、理解度を確認しましょう:

  • 基礎: context.Background()context.TODO()の違いは何ですか?実装とセマンティクスの観点から説明してください。
  • キャンセル伝播: 親コンテキストをキャンセルすると、子コンテキストはどのように停止しますか?内部実装を説明してください。
  • Done チャネル: なぜDone()chan struct{}ではなく<-chan struct{}を返すのですか?
  • タイムアウト: WithTimeout(parent, 10s)で作った子コンテキストを、手動で5秒後にキャンセルした場合、ctx.Err()は何を返しますか?
  • デッドライン継承: 親が5秒、子が10秒のタイムアウトを設定した場合、子は実際に何秒でタイムアウトしますか?
  • WithValue: context.WithValue()を10回ネストした場合、Value()の時間計算量はいくつですか?
  • HTTPリクエスト: HTTPハンドラー内でr.Context()を取得すると、どのタイミングで自動キャンセルされますか?
  • データベース: db.QueryContext(ctx, query)を実行中にコンテキストがキャンセルされると、データベース側では何が起こりますか?
  • メモリ: cancelCtxvalueCtx、どちらがメモリ使用量が多いですか?理由も説明してください。
  • エラー: context.Canceledcontext.DeadlineExceededの違いを、発生条件とともに説明してください。
  • 並行処理: 5つのgoroutineが同じコンテキストを監視している場合、cancel()を1回呼ぶだけで全て停止できるのはなぜですか?
  • ベストプラクティス: なぜコンテキストをstructのフィールドに保存してはいけないのですか?
  • まとめ

    本章では、contextパッケージの深層を学びました。

    🔑 重要ポイント

  • インターフェース: シンプルな4メソッドで複雑な制御を実現
  • キャンセル伝播: 親から子への自動的な伝播メカニズム
  • タイムアウト: time.Timerによる自動キャンセル
  • 値の伝播: 連結リスト構造による値の検索
  • HTTPとの統合: リクエストライフサイクルとの自然な統合
  • データベース: クエリキャンセルによるリソース保護
  • パフォーマンス: メモリオーバーヘッドと最適化
  • エラーハンドリング: キャンセルとタイムアウトの区別

💡 次のステップ: 次章では、これまで学んだすべての知識を統合して、本番環境で使える実践的なアプリケーションを構築します。コンテキストは、そのすべてのレイヤーで活用されます。

⚠️ 本番環境への注意: すべての長時間処理には必ずコンテキストを渡し、適切なタイムアウトとキャンセル処理を実装してください。これにより、システムの堅牢性と応答性が大幅に向上します。