見出し画像

Golang_コンテキストとは #473

サーバーを動かすにはリクエストを処理する方法が必要で、リクエストを処理するには個々のリクエストのメタデータを処理する方法が必要です。

Goではコンテキスト(context)という概念を使ってリクエストのメタデータを管理しています。リクエストのメタデータとは、主に以下の2つを指します。

  • 正しく処理するためのメタデータ:タイムアウトやデッドラインなど、リクエストがどのように処理されるべきかを指示する情報

  • 処理を中止するためのメタデータ:リクエストのキャンセルシグナルなど、処理を中止するための情報

Goで扱うコンテキストは、APIの境界を超えて(あるいはプロセス間で)次の3つのものを「持ち運ぶ」ための機構です。

  1. 処理のデッドライン(time.Time)

  2. キャンセレーションのシグナル

  3. その他の処理に必要な値(ゴルーチン間で値を共有する等)

使い方を見ていきます。

コンテキストの使い方

context.Contextインターフェース

Goにおけるコンテキストは何か新しい特別な機構ではなく、単なるインターフェースです。context.Contextインターフェースには以下のメソッドがあります

  • Deadline() (deadline time.Time, ok bool)

  • Done() <-chan struct{}

  • Err() error

  • Value(key interface{}) interface{}

コンテキストの生成

コンテキストは通常、context.Background()またはcontext.TODO()で生成されます。

context.Background()は空の初期コンテキストを生成します。
context.TODO()は具体的な使用場所が決まっていない場合にプレースホルダーとして置きます。

ctx := context.Background()  // 初期コンテキストを生成

または

ctx := context.TODO()  // こちらは開発用であり最終的なコードには含めない

生成した空のコンテキストが出発点になり、これを関数に渡していくことで途中で処理を中断したり、値を受け渡したりできます。

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

コンテキストにキャンセル機能を付加できます。上記で生成したコンテキストに以下のようにして付加できます。

ctx, cancel := context.WithCancel(ctx)
defer cancel  // 関数の終了時にキャンセルを呼び出す
 
// ゴルーチン内でキャンセルを待つ
go func() {
    // 何か処理をしつつ
    cancel()  // 必要に応じてキャンセルを呼び出す
}

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

タイムアウトを設定して、ゴルーチンで①処理の完了、または②タイムアウトを待機してそれぞれハンドリングできます。

context.WithTimeoutで作成したコンテキストは、内部的にデッドラインを持つようになり、そのデッドラインを過ぎてタイムアウトするとDone()メソッドを実行してチャネルを返し、終了を通知します。

ctx := context.Background()

// 5秒のタイムアウト付きコンテキストを生成
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
 
// 処理を行うゴルーチンを開始
done := make(chan struct{})
go func() {
    // 例として2秒の操作をシミュレート
    time.Sleep(2 * time.Second)
    // ゴルーチンの完了を通知
    close(done)
}()

// タイムアウトまたは完了を待つ
select {
case <-ctx.Done():
    // コンテキストがキャンセルされた場合(タイムアウトまたは明示的なキャンセル)
    fmt.Println("タイムアウトしました:", ctx.Err())
case <-done:
    // 処理が完了した場合
    fmt.Println("処理が完了しました")
}

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

タイムアウト付きとの違いは、こちらは日時で終了期限を区切る点です。使い方はタイムアウト付きと似ています。

deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()  // 関数の終了時にキャンセルを呼び出す

コンテキストに値を追加

キーバリューで値を追加することもできます。

ctx := context.Background()
// コンテキストに値を追加
key := "exampleKey"
value := "exampleValue"
ctx = context.WithValue(ctx, key, value)

追加した値は以下のようにして取り出せます。

val := ctx.Value(key)
if val != nil {
    fmt.Println("Value:", val)
} else {
    fmt.Println("Key not found")
}


ネットワークリクエストでの利用例

コンテキストはネットワークリクエストやデータベース操作など、キャンセル可能な操作に対して非常に有用です。

Goの標準ライブラリであるnet/httpパッケージがコンテキストの概念をサポートするように設計されており、HTTPサーバーのリクエストごとにコンテキストを扱うことができます。

package main

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

func handler(w http.ResponseWriter, r *http.Request) {
    // リクエストのコンテキストを取得
    ctx := r.Context()

    // 処理を行うゴルーチンを開始
    go func() {
        select {
        case <-time.After(2 * time.Second):
            fmt.Fprintln(w, "処理が完了しました")
        case <-ctx.Done():
            fmt.Fprintln(w, "リクエストがキャンセルされました:", ctx.Err())
        }
    }()
}

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

この例ではゴルーチンを使用した2秒間の非同期処理をシミュレートしています。そしてselect文で<-ctx.Done()を待機することで、コンテキストのキャンセルやタイムアウトを監視しています。

コンテキストの慣例

コンテキストを実装する際の慣例としては以下のようなものがあります。

  • コンテキストを積極的にキャンセル: 不要になったコンテキストは必ずキャンセルします。特にタイムアウトやデッドラインがある場合はdefer cancel()で確実にキャンセルすることが重要です。

  • コンテキストは第1引数に渡す: 関数やメソッドの第1引数として渡すのが慣習です。

  • 不要な値をコンテキストに詰め込まない: コンテキストはキャンセルシグナルやデッドラインを伝播するためのものであり、データストアとして使うべきではありません。必要最小限の情報のみを含めるようにします。


ここまでお読みいただきありがとうございました!!

参考

この記事が気に入ったらサポートをしてみませんか?