Go 言語の context を基礎から実践まで解説

# はじめに

こんにちは、エンジニアの清水です。

私は業務で Go を書いているのですが、context についての理解が浅いことでエラーに遭遇したので、勉強のために記事を書いてみました。

この記事では、実際のコード例を交えながら Go 言語の context パッケージの基本から実践的な使用方法まで解説していきます。
また実際に私が遭遇したエラーの例も交えて context の陥りやすい落とし穴についても解説します。

# context の基本

## context とは何か

`context`は、Go の標準ライブラリに含まれるパッケージで、API やプロセス間でリクエストスコープの値、キャンセル信号、デッドラインなどを伝播するための仕組みを提供します。

## なぜ context が必要なのか

1. リソースの適切な管理：不要になった処理を適切にキャンセルし、メモリや CPU などのリソースを解放できます。
2. タイムアウト制御：長時間実行される処理に対して、適切なタイムアウトを設定できます。
3. リクエストスコープの値の伝播：HTTP リクエストのトレース ID（リクエストを追跡するための一意の識別子）など、処理全体で共有すべき値を安全に伝達できます。

## context の主な用途

- HTTP リクエストのキャンセル制御：クライアントがリクエストをキャンセルした場合に、サーバー側の処理も中止できます。
- データベースクエリのタイムアウト設定：長時間実行されるクエリに制限時間を設けることができます。
- goroutine 間でのキャンセル信号の伝播：複数の goroutine 間で処理の中止を連携できます。
- マイクロサービス間での情報伝達：複数の小さなサービス間でリクエスト情報を受け渡せます。

# context の使用方法

## 基本的な使い方

```go
package main

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

func doSomething(ctx context.Context) error {
    // ctxを使用して処理を実行
    select {
    case <-ctx.Done():
        // contextがキャンセルされた場合の処理
        // ctx.Done()はcontextがキャンセルされたときに閉じられるチャネルを返す
        return ctx.Err()
    default:
        // 通常の処理
        // contextがキャンセルされていない場合はここが実行される
        fmt.Println("処理を実行中...")
        time.Sleep(2 * time.Second) // 処理のシミュレーション
        fmt.Println("処理が完了しました")
        return nil
    }
}

func main() {
    ctx := context.Background()
    err := doSomething(ctx)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    }
}
```

この例では、`select`文を使って context のキャンセル状態を確認しています。`ctx.Done()`は context がキャンセルされたときに閉じられるチャネル（goroutine 間の通信に使用される仕組み）を返します。

## タイムアウトの設定

```go
package main

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

func doSomething(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        fmt.Println("処理を実行中...")
        time.Sleep(2 * time.Second)
        fmt.Println("処理が完了しました")
        return nil
    }
}

func main() {
    // context.Background()は空のcontextを作成
    // WithTimeoutは指定した時間後にキャンセルされるcontextを作成
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    // 関数終了時に必ずcancelを呼び出す
    defer cancel()

    err := doSomething(ctx)
    if err != nil {
        log.Printf("Error: %v", err)
    }
}
```

ここでは、5 秒のタイムアウトを設定しています。`context.Background()`は空の context を作成し、それを基に新しい context を作成しています。`defer cancel()`は関数終了時に必ず cancel を呼び出すようにしています。

## キャンセル処理の実装

```go
package main

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

func main() {
    // キャンセル可能なcontextを作成
    ctx, cancel := context.WithCancel(context.Background())
    go func() {
        // 何らかの条件でキャンセルを実行
        time.Sleep(2 * time.Second)
        cancel() // 2秒後にcontextをキャンセル
    }()

    select {
    case <-ctx.Done():
        fmt.Println("処理がキャンセルされました")
    case <-time.After(5 * time.Second):
        fmt.Println("処理が完了しました")
    }
}
```

この例では、goroutine を使って 2 秒後に context をキャンセルしています。メイン処理では`select`文を使って、context のキャンセルと 5 秒のタイムアウトのどちらが先に発生するかを待っています。

## 値の受け渡し

```go
package main

import (
    "context"
    "fmt"
)

func main() {
    // contextに値を関連付ける
    ctx := context.WithValue(context.Background(), "key", "value")

    // 値の取得
    if v, ok := ctx.Value("key").(string); ok {
        fmt.Println(v) // "value"
    }
}
```

`context.WithValue`を使うと、context に値を関連付けることができます。ここでは、"key"というキーに"value"という値を関連付けています。

# context の落とし穴と注意点

## API の context を長時間実行処理に渡す危険性

私が実際に経験した問題です。API の context をファイルエクスポート処理のような長時間実行される処理に渡してしまうと、API レスポンスが返った時点で context がキャンセルされ、処理が中断されてしまう可能性があります。

### 誤った使用例

```go
package main

import (
    "context"
    "net/http"
)

func handleExport(w http.ResponseWriter, r *http.Request) {
    // 誤った使用例
    go exportFile(r.Context()) // APIのcontextを長時間処理に渡している
    w.WriteHeader(http.StatusOK)
}

func exportFile(ctx context.Context) {
    // 長時間の処理
    // ctx.Done()が呼ばれると中断される可能性がある
    // APIレスポンスが返った後にcontextがキャンセルされる可能性がある
}

func main() {
    http.HandleFunc("/export", handleExport)
    http.ListenAndServe(":8080", nil)
}
```

この例では、HTTP リクエストの context をそのままファイルエクスポート処理に渡しています。これにより、API レスポンスが返された後に context がキャンセルされ、エクスポート処理が途中で中断される可能性があります。

### 正しい使用例

```go
package main

import (
    "context"
    "net/http"
    "time"
)

func handleExport(w http.ResponseWriter, r *http.Request) {
    // 新しいcontextを作成（APIのcontextとは独立）
    ctx := context.Background()
    go exportFile(ctx)
    w.WriteHeader(http.StatusOK)
}

func exportFile(ctx context.Context) {
    // 長時間の処理
    // 必要に応じてタイムアウトを設定
    ctx, cancel := context.WithTimeout(ctx, 1*time.Hour)
    defer cancel() // 処理終了時にcancelを呼び出す

    // 処理の実行
    // このcontextはAPIレスポンスとは独立しているため、
    // APIレスポンスが返された後も処理が継続される
}

func main() {
    http.HandleFunc("/export", handleExport)
    http.ListenAndServe(":8080", nil)
}
```

この例では、API の context ではなく新しい context を作成してファイルエクスポート処理に渡しています。これにより、API レスポンスが返された後もエクスポート処理が継続できます。

## goroutine での context 使用時の注意点

goroutine 内で context を使用する際は、親の context がキャンセルされた場合の動作を適切に処理する必要があります。

```go
package main

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

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

    go childFunction(ctx)

    // 何らかの条件でキャンセルを実行
    time.Sleep(2 * time.Second)
    cancel() // 2秒後にcontextをキャンセル
}

func childFunction(ctx context.Context) {
    select {
    case <-ctx.Done():
        fmt.Println("親のcontextがキャンセルされました")
        return
    case <-time.After(5 * time.Second):
        fmt.Println("処理が完了しました")
    }
}

func main() {
    parentFunction()
}
```

この例では、`parentFunction`内で作成された context を`childFunction`に渡しています。親の context がキャンセルされると、子の goroutine も適切に終了します。

## context のキャンセルタイミングの重要性

context のキャンセルタイミングを適切に管理することが重要です。早すぎるキャンセルや、キャンセルし忘れによるリソースリーク（プログラムが使用しているリソースが適切に解放されない状態）に注意が必要です。

```go
package main

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

func processWithTimeout() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 関数終了時に必ずcancelを呼び出す

    select {
    case <-ctx.Done():
        fmt.Println("タイムアウトまたはキャンセルされました:", ctx.Err())
    case <-time.After(10 * time.Second):
        fmt.Println("処理が完了しました")
    }
}

func main() {
    processWithTimeout()
}
```

この例では、5 秒のタイムアウトを設定しています。`defer cancel()`を使用して、関数終了時に必ずキャンセルを呼び出すことで、リソースリークを防いでいます。タイムアウトが発生すると、`ctx.Done()`が閉じられ、適切に処理が中断されます。

# 実践的な使用例

## 外部サービス呼び出し時の context 管理

```go
package main

import (
    "context"
    "net/http"
    "time"
)

func callExternalService(ctx context.Context) error {
    // 10秒のタイムアウトを設定
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel() // 関数終了時にcancelを呼び出す

    // contextを含むHTTPリクエストを作成
    req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com", nil)
    if err != nil {
        return err
    }

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

    // レスポンスの処理
    return nil
}

func main() {
    ctx := context.Background()
    err := callExternalService(ctx)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
    }
}
```

この例では、外部サービスの呼び出しに 10 秒のタイムアウトを設定しています。`http.NewRequestWithContext`を使用して、HTTP リクエストに context を関連付けています。

## 長時間実行処理での context 作成方法

```go
package main

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

func longRunningTask() {
    // 基本となるcontextを作成
    ctx := context.Background()
    // キャンセル可能なcontextを作成
    ctx, cancel := context.WithCancel(ctx)
    defer cancel() // 関数終了時にcancelを呼び出す

    go func() {
        // 何らかの条件でキャンセルを実行
        time.Sleep(1 * time.Hour)
        cancel() // 1時間後にcontextをキャンセル
    }()

    // 長時間の処理
    for {
        select {
        case <-ctx.Done():
            fmt.Println("処理がキャンセルされました")
            return
        default:
            // 処理の実行
            // contextがキャンセルされていない場合はここが実行される
        }
    }
}

func main() {
    longRunningTask()
}
```

この例では、長時間実行される処理のための context を作成しています。goroutine を使って 1 時間後に context をキャンセルし、メインの処理ループ内で context のキャンセル状態を定期的にチェックしています。

# まとめ

context の適切な使用は、Go を書く上で重要だと感じました。API の context を長時間実行処理に渡すなどの落とし穴に注意しながら、適切に context を管理することで、より堅牢なアプリケーションを構築することができると思いました。

## 効果的な使用のためのベストプラクティス

1. 適切なタイムアウトの設定
2. キャンセル処理の適切な実装
3. goroutine での慎重な使用
4. 長時間実行処理には独立した context の作成

# 参考資料

- [Go 公式ドキュメント - context パッケージ](https://pkg.go.dev/context)
- [Go Blog - Go Concurrency Patterns: Context](https://go.dev/blog/context)
- [Effective Go](https://go.dev/doc/effective_go)