第22章 コンテキスト

contextとは

Go言語におけるcontextパッケージは、並行処理の実行中に複数のゴルーチン間でキャンセル信号、タイムアウト、デッドライン、リクエストスコープの値を伝達するための強力なツールです。このセクションでは、contextの基本概念、その重要性、および使用シナリオについて、具体的な事例を交えて説明します。


コンテキストの基本概念

context.Contextは、ゴルーチン間でのデータの伝達と、長期実行操作のキャンセルを管理するためのインターフェースです。このインターフェースは、以下のようなメソッドを提供します。

  • Done(): コンテキストがキャンセルされたかどうかを示すチャネルを返します。
  • Err(): コンテキストがキャンセルされた理由を返します。
  • Deadline(): コンテキストのデッドラインが設定されていれば、その時刻と、デッドラインが設定されているかのbool値を返します。
  • Value(key interface{}): コンテキストに関連付けられた値を返します。

コンテキストの基本概念

contextは、以下の理由で非常に重要です。

  • キャンセル伝播: 複数のゴルーチン間で一つの操作をキャンセルする際に、その信号を効率的に伝達できます。
  • タイムアウトとデッドライン: 特定の操作に対して最大実行時間を設定し、それを超えると自動的にキャンセルします。
  • 値の伝達: リクエストスコープの値や設定をゴルーチン間で共有します。

コンテキストの使用シナリオ


HTTPリクエストの処理

HTTPサーバーでリクエストを処理する際、各リクエストには独自のコンテキストがあり、それを使用してリクエストのキャンセルやタイムアウトを管理できます。

package main

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

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(5 * time.Second):
        fmt.Fprintf(w, "response")
    case <-ctx.Done():
        err := ctx.Err()
        fmt.Println("server:", err)
        internalError := http.StatusInternalServerError
        http.Error(w, err.Error(), internalError)
    }
}

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

この例では、リクエストが5秒以内に完了するか、クライアントによってキャンセルされるかを待ちます。


データベースクエリのキャンセル

長時間実行されるデータベースクエリをキャンセルする必要がある場合、コンテキストを使用してクエリの実行を管理できます。

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM my_table")
if err != nil {
    // エラーハンドリング
}
defer rows.Close()

この例では、1秒後にタイムアウトするコンテキストを使用してデータベースクエリを実行しています。タイムアウトが発生すると、クエリは自動的にキャンセルされます。


contextパッケージの基本

Go言語のcontextパッケージは、ゴルーチン間でキャンセル信号やデッドライン、任意の値を伝えるために設計されています。これは特に、長時間実行される処理や、リクエストのライフサイクルを管理する際に有用です。ここでは、contextパッケージの基本について、具体的な事例を交えて説明します。


context.Contextインターフェース

context.Contextは、Done(), Err(), Deadline(), Value()の4つのメソッドを持つインターフェースです。これらのメソッドを使って、コンテキストがキャンセルされたか監視したり、特定のデータをコンテキストに格納・取得したりします。


コンテキストの作成

ルートコンテキストの作成

context.Background()context.TODO()は、ルートコンテキストを生成します。これらは、アプリケーションの最上位またはグローバルなコンテキストとして使用されます。

ctx := context.Background()


キャンセル可能なコンテキストの作成

context.WithCancel(parent)は、親コンテキストから派生したキャンセル可能なコンテキストを作成します。このコンテキストは、cancel()関数を呼び出すことで明示的にキャンセルできます。

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

// キャンセルを伝播させる処理
go func() {
    // 何かの処理
    cancel() // 処理が完了したらキャンセル
}()

<-ctx.Done() // キャンセルが完了するのを待つ


デッドライン付きコンテキストの作成

context.WithDeadline(parent, deadline)は、指定した時間(deadline)に自動でキャンセルされるコンテキストを作成します。

deadline := time.Now().Add(1 * time.Hour)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

// 何か時間がかかる処理


タイムアウト付きコンテキストの作成

context.WithTimeout(parent, timeout)は、指定した時間(timeout)が経過すると自動でキャンセルされるコンテキストを作成します。これはWithDeadlineの特殊なケースです。

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// タイムアウトするかもしれない処理


コンテキストへの値の格納

context.WithValue(parent, key, val)を使用して、コンテキストに任意の値を格納します。これは主に、リクエストスコープのデータを渡すために使用されますが、乱用を避けるべきです。

ctx := context.WithValue(context.Background(), "userID", "abc123")
userID := ctx.Value("userID").(string)


コンテキストのベストプラクティス

Go言語におけるcontextパッケージの使用は、特に並行処理やリソース管理において重要な役割を果たします。適切に使用することで、プログラムのキャンセルやタイムアウトの管理、リクエストスコープの値の伝達が容易になります。ここでは、contextパッケージのベストプラクティスについて、具体的な事例を交えて説明します。


コンテキストの伝播

コンテキストは、プログラムの各層を通じて伝播されるべきです。HTTPリクエストの処理を開始する際に作成されたコンテキストは、データベースへのクエリ、外部APIへのリクエストなど、リクエスト処理に関わる全ての操作に渡されるべきです。

事例: HTTPハンドラーでのコンテキストの伝播

func myHandler(w http.ResponseWriter, r *http.Request) {
    // リクエストからコンテキストを取得
    ctx := r.Context()
    
    // コンテキストを伝播させる
    result, err := fetchData(ctx, "http://example.com/data")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    fmt.Fprintf(w, "Result: %s", result)
}

func fetchData(ctx context.Context, url string) (string, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return "", err
    }
    return string(data), nil
}


コンテキストのキャンセルシグナルの監視

作成されたコンテキストのキャンセルシグナルを適切に監視し、キャンセルが発生したら速やかに処理を中断することが重要です。これにより、無駄な処理を避け、リソースを解放できます。

事例: キャンセルシグナルの監視

func process(ctx context.Context) {
    select {
    case <-ctx.Done():
        // コンテキストがキャンセルされた場合の処理
        fmt.Println("Process canceled")
        return
    case <-time.After(5 * time.Minute):
        // 重い処理の模擬
    }
}


コンテキストに値を格納する際の注意

context.WithValueは便利ですが、乱用を避けるべきです。主に設定やリクエスト固有の情報など、リクエストスコープのデータの伝達に限定して使用することが推奨されます。

事例: WithValueの適切な使用

func main() {
    ctx := context.Background()
    // リクエストIDをコンテキストに格納
    ctx = context.WithValue(ctx, "requestID", "abc123")

    processRequest(ctx)
}

func processRequest(ctx context.Context) {
    // コンテキストからリクエストIDを取得
    requestID := ctx.Value("requestID").(string)
    fmt.Println("Processing request with ID:", requestID)
}


コンテキストを使用した並行処理パターン

Go言語におけるcontextパッケージを利用した並行処理パターンは、複雑なシステムでも柔軟かつ効果的なリソース管理を実現します。ここでは、コンテキストを使用して並行処理を行う二つの典型的なパターン、「キャンセル可能な処理の実行」と「タイムアウト付きの処理の実行」について、具体的な事例を交えて説明します。


キャンセル可能な処理の実行

多くの並行処理では、特定の条件下(ユーザーのリクエストキャンセル、アプリケーションのシャットダウン、エラー発生など)で実行中の処理を早期に中断する必要があります。contextパッケージを使用すると、これらのシナリオでゴルーチンのキャンセルを簡単に実現できます。

事例: キャンセル可能なHTTPリクエスト

package main

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

// 模擬的な長時間実行関数
func longRunningTask(ctx context.Context) error {
    // HTTPリクエストの生成(キャンセル可能)
    req, _ := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil)
    
    // HTTPクライアントの生成
    client := &http.Client{}
    
    // リクエストの送信
    resp, err := client.Do(req)
    if err != nil {
        return err // エラーのハンドリング
    }
    defer resp.Body.Close()
    
    // リクエストが成功したことを示す
    fmt.Println("Request successfully completed")
    return nil
}

func main() {
    // コンテキストの生成(キャンセル可能)
    ctx, cancel := context.WithCancel(context.Background())
    
    // 2秒後に処理をキャンセル
    time.AfterFunc(2*time.Second, func() {
        fmt.Println("Cancelling context")
        cancel()
    })

    // 長時間実行関数の呼び出し
    err := longRunningTask(ctx)
    if err != nil {
        fmt.Println("Task failed:", err)
    }
}

この例では、context.WithCancelを使用してキャンセル可能なコンテキストを生成し、2秒後にcancel()関数を呼び出しています。これにより、HTTPリクエストが途中でキャンセルされます。


タイムアウト付きの処理の実行

処理が指定された時間内に完了することを保証したい場合には、context.WithTimeoutを使用してタイムアウト付きのコンテキストを生成します。タイムアウトに達すると、コンテキストは自動的にキャンセルされます。

事例: タイムアウト付きのデータベースクエリ

package main

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

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

func main() {
    db, err := sql.Open("sqlite3", "./example.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // タイムアウト付きのコンテキストを作成
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()

    // データベースクエリを実行
    _, err = db.ExecContext(ctx, "SELECT * FROM my_table")
    if err != nil {
        fmt.Println("Query cancelled or timed out")
    }
}

db.ExecContextメソッドを使用することで、contextに基づいてSQLクエリの実行を制御できます。この例では、1秒のタイムアウトが設定されており、この時間内にクエリが完了しない場合、自動的にキャンセルされます。


コンテキストとエラーハンドリング

Go言語でのエラーハンドリングは、プログラムの堅牢性を高める上で非常に重要です。contextパッケージを使用すると、エラーハンドリングをさらに強化し、特に並行処理やリクエストのライフサイクル管理において柔軟に対応できます。ここでは、コンテキストとエラーハンドリングに関する具体的な事例を紹介します。


コンテキストのキャンセルとエラーハンドリング

コンテキストがキャンセルされた場合、context.ContextインターフェースのErr()メソッドを使って、その原因を特定することができます。context.Canceledcontext.DeadlineExceededなど、contextパッケージが提供するエラーをチェックすることにより、適切なエラーレスポンスを返したり、特定のクリーンアップ処理を行うことができます。

事例: HTTPリクエストのキャンセル

package main

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

func longRunningOperation(ctx context.Context) error {
    // ここでは例として、10秒後に完了する長い処理を想定
    select {
    case <-time.After(10 * time.Second):
        fmt.Println("Operation completed")
    case <-ctx.Done():
        // コンテキストがキャンセルされた場合
        return ctx.Err()
    }
    return nil
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    err := longRunningOperation(ctx)
    if err != nil {
        // エラーに応じた処理
        if err == context.Canceled {
            http.Error(w, "Request Cancelled", http.StatusBadRequest)
        } else if err == context.DeadlineExceeded {
            http.Error(w, "Deadline Exceeded", http.StatusGatewayTimeout)
        } else {
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        }
        return
    }
    fmt.Fprintln(w, "Operation Successful")
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("Server started on port 8080")
    http.ListenAndServe(":8080", nil)
}

この例では、longRunningOperation関数内でコンテキストのキャンセルを検出し、ctx.Err()でエラーの種類を確認しています。この情報に基づいて、HTTPハンドラーでは適切なHTTPステータスコードでレスポンスを返しています。


コンテキストとデータベースクエリ

データベース操作においても、コンテキストを用いてタイムアウトやキャンセルを管理できます。データベースクエリがタイムアウトまたはキャンセルされた場合には、適切なエラーハンドリングが求められます。

事例: コンテキストを使用したデータベースクエリ

package main

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

    _ "github.com/go-sql-driver/mysql" // MySQLドライバーのインポート
)

func main() {
    // データベースへの接続を開く
    db, err := sql.Open("mysql", "user:password@/dbname")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // タイムアウト付きのコンテキストを作成
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // タイムアウトまたはキャンセルされるまでクエリを実行
    query := "SELECT name FROM users WHERE id = ?"
    rows, err := db.QueryContext(ctx, query, 1)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    // 結果を処理
    for rows.Next() {
        var name string
        if err := rows.Scan(&name); err != nil {
            log.Fatal(err)
        }
        fmt.Println(name)
    }

    // タイムアウトに到達したかチェック
    if ctx.Err() == context.DeadlineExceeded {
        fmt.Println("Query cancelled due to timeout.")
    }
}

この例では、5秒のタイムアウトを設定しています。クエリの実行が5秒以内に完了しない場合、context.DeadlineExceededエラーが発生し、クエリの実行が中断されます。これにより、不要なリソース消費を避け、アプリケーションのパフォーマンスを維持できます。


実践的な応用例

Go言語の強力な並行処理機能は、多種多様な実践的応用例に利用できます。ここでは、Go言語での「リアルタイムWebサーバーの構築」と「コンカレントなファイル処理」に焦点を当て、具体的な事例を交えて説明します。


リアルタイムWebサーバーの構築

Go言語は、その軽量なゴルーチンと効率的なチャネル通信システムを活用して、高性能なリアルタイムWebサーバーを構築するのに適しています。例えば、WebSocketを使用してリアルタイムチャットアプリケーションを実装することができます。

WebSocketを使用したリアルタイムチャットアプリケーション

package main

import (
    "net/http"
    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func chatHandler(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        return
    }
    defer conn.Close()

    for {
        messageType, p, err := conn.ReadMessage()
        if err != nil {
            return
        }
        // クライアントからのメッセージをブロードキャストする
        conn.WriteMessage(messageType, p)
    }
}

func main() {
    http.HandleFunc("/chat", chatHandler)
    http.ListenAndServe(":8080", nil)
}

この簡単な例では、WebSocket接続を介してクライアントから送信されたメッセージを受信し、同じメッセージをクライアントに返送しています。本番環境では、接続管理やメッセージのブロードキャストに追加のロジックが必要になりますが、Goのゴルーチンとチャネルを使えば、これらの処理を効率的に実装できます。


コンカレントなファイル処理

大量のファイルを扱う処理も、Go言語の並行処理機能を用いることで、パフォーマンスを大幅に向上させることができます。例えば、ディレクトリ内の全てのファイルに対して同時に何らかの処理を行いたい場合に役立ちます。

ファイルの同時処理

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "sync"
)

func processFile(wg *sync.WaitGroup, filename string) {
    defer wg.Done()

    // ここでファイルを処理する
    fmt.Println("Processing", filename)
}

func main() {
    var wg sync.WaitGroup

    files, err := ioutil.ReadDir(".")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    for _, file := range files {
        wg.Add(1)
        go processFile(&wg, file.Name())
    }

    wg.Wait() // すべてのゴルーチンが完了するのを待つ
    fmt.Println("Completed processing all files.")
}

このコードスニペットでは、現在のディレクトリ内の各ファイルに対してprocessFile関数を非同期に実行しています。sync.WaitGroupを使用して、すべてのファイルの処理が完了するまでプログラムの終了を遅延させています。これにより、ファイル処理のパフォーマンスが向上します。


練習問題1.

HTTPリクエストを実行する簡単なGoプログラムを書き、2秒後にリクエストをキャンセルするようにしてください。リクエストがキャンセルされた場合と、正常にレスポンスを受け取った場合の両方で、適切なメッセージをコンソールに出力してください。


練習問題2.

タイムアウト付きでデータベースクエリを実行するプログラムを書いてください。クエリの実行に3秒かかるものとし、コンテキストのタイムアウトは1秒後に設定してください。タイムアウトが発生した場合、"Query timed out"というメッセージを出力してください。


練習問題3.

親ゴルーチンから子ゴルーチンにコンテキストを伝播させ、親ゴルーチンでコンテキストをキャンセルしたときに、子ゴルーチンがそのキャンセルを検出して終了するプログラムを書いてください。子ゴルーチンがキャンセルを検出したことを示すメッセージを出力してください。