Go言語: Contextパッケージとテスト戦略

By quonta 4月 13, 2024

Contextパッケージの基本

Go言語のcontextパッケージは、API境界やプロセス間でキャンセル信号、タイムアウト、値の受け渡しを行うためのメカニズムを提供します。

Contextの作成

contextパッケージは、context.Background()context.TODO()の2つの関数を提供しています。これらの関数は、新しいContextを作成します。

ctx := context.Background()

Contextに値を設定する

context.WithValue()関数を使用して、Contextに値を設定できます。この関数は新しいContextを返します。

ctx := context.WithValue(context.Background(), key, value)

Contextから値を取得する

Contextから値を取得するには、Value()メソッドを使用します。

value := ctx.Value(key)

Contextのキャンセルとタイムアウト

context.WithCancel()関数は、親Contextをキャンセル可能なContextに変換します。この関数は新しいContextとそのContextをキャンセルするための関数を返します。

ctx, cancel := context.WithCancel(parentCtx)

context.WithTimeout()関数は、指定した時間が経過したら自動的にキャンセルされるContextを作成します。

ctx, cancel := context.WithTimeout(parentCtx, timeout)

以上がGo言語のcontextパッケージの基本的な使い方です。このパッケージを理解し、適切に使用することで、Goの並行処理をより効果的に制御することができます。次のセクションでは、これらの概念を使用してユニットテストを書く方法について説明します。

Contextを使用したユニットテスト

Go言語のcontextパッケージは、ユニットテストにおいても非常に有用です。特に、非同期操作やタイムアウトを伴う関数のテストにおいて、contextパッケージは重要な役割を果たします。

テストの準備

まず、テスト対象の関数がContextを引数として受け取るようにします。これにより、テスト時に任意のContextを渡すことができます。

func DoSomething(ctx context.Context, arg ArgType) (ResultType, error) {
    // ...
}

タイムアウトを伴うテスト

context.WithTimeout()を使用して、関数が指定した時間内に終了することを確認するテストを書くことができます。

func TestDoSomething(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()

    result, err := DoSomething(ctx, arg)
    if err != nil {
        t.Fatalf("expected nil error, got %v", err)
    }

    // Check the result
    // ...
}

このテストでは、DoSomething関数が100ミリ秒以内に終了しない場合、Contextがキャンセルされ、関数はエラーを返します。

キャンセルを伴うテスト

同様に、context.WithCancel()を使用して、関数がContextのキャンセルに適切に反応することを確認するテストを書くことができます。

func TestDoSomethingWithCancel(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())

    go func() {
        time.Sleep(50 * time.Millisecond)
        cancel()
    }()

    _, err := DoSomething(ctx, arg)
    if err != context.Canceled {
        t.Fatalf("expected context.Canceled, got %v", err)
    }
}

このテストでは、DoSomething関数がContextのキャンセルに適切に反応し、context.Canceledエラーを返すことを確認します。

以上が、Go言語のcontextパッケージを使用したユニットテストの基本的な書き方です。これらのテストパターンを理解し、適切に使用することで、Goの並行処理を含むコードの品質を向上させることができます。次のセクションでは、HTTPリクエストとContextの関連性について説明します。

ContextとHTTPリクエスト

Go言語のcontextパッケージは、HTTPリクエストの処理においても重要な役割を果たします。特に、リクエストのライフサイクル全体にわたるキャンセルやタイムアウトの制御、リクエスト固有の情報の受け渡しに使用されます。

HTTPリクエストとContextの関連性

Goのnet/httpパッケージのRequest型は、Context()メソッドを持っています。これにより、各リクエストはそれ自体がContextを持つことができます。

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    // ...
}

Contextを使用したリクエストのキャンセル

HTTPリクエストのContextは、クライアントが接続を閉じたときに自動的にキャンセルされます。これにより、リクエストの処理を中断することができます。

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(2 * time.Second):
        fmt.Fprintln(w, "Hello")
    case <-ctx.Done():
        fmt.Fprint(w, "Request cancelled")
    }
}

このハンドラは、2秒後に”Hello”をレスポンスとして返します。しかし、クライアントが接続を閉じると、ctx.Done()チャネルが閉じられ、”Request cancelled”がレスポンスとして返されます。

Contextを使用したリクエスト固有の情報の受け渡し

Contextは、リクエスト固有の情報(例えば、認証情報やリクエストIDなど)をミドルウェアやハンドラ間で受け渡すのにも使用できます。

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := context.WithValue(r.Context(), "key", "value")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

以上が、Go言語のcontextパッケージとHTTPリクエストの関連性についての基本的な説明です。この知識を活用することで、リクエストのライフサイクルをより効果的に制御し、リクエスト固有の情報を適切に管理することができます。次のセクションでは、Contextのキャンセルとタイムアウトについて詳しく説明します。

Contextのキャンセルとタイムアウト

Go言語のcontextパッケージは、キャンセルとタイムアウトの制御において重要な役割を果たします。これにより、長時間実行される操作を適切に制御することができます。

Contextのキャンセル

context.WithCancel(parent Context)関数は、新しいContextとそのContextをキャンセルするための関数を返します。このキャンセル関数を呼び出すと、関連するContextとその子Contextはすぐにキャンセルされます。

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

Contextのタイムアウト

context.WithTimeout(parent Context, timeout time.Duration)関数は、新しいContextとそのContextをキャンセルするための関数を返します。この関数は、指定した時間が経過したら自動的にContextをキャンセルします。

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Important: Always defer the cancellation function to avoid leaks!
// ...

Contextのデッドライン

context.WithDeadline(parent Context, d time.Time)関数は、新しいContextとそのContextをキャンセルするための関数を返します。この関数は、指定した時刻になったら自動的にContextをキャンセルします。

deadline := time.Now().Add(2 * time.Hour)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel() // Important: Always defer the cancellation function to avoid leaks!
// ...

以上が、Go言語のcontextパッケージにおけるキャンセルとタイムアウトの基本的な使い方です。これらの機能を理解し、適切に使用することで、Goの並行処理をより効果的に制御することができます。次のセクションでは、Contextを使用したエラーハンドリングについて説明します。

Contextを使用したエラーハンドリング

Go言語のcontextパッケージは、エラーハンドリングにおいても重要な役割を果たします。特に、非同期操作や並行処理を行う際に、エラーの伝播と処理を効率的に行うことができます。

Contextのキャンセルとエラーハンドリング

Contextがキャンセルされると、ctx.Done()チャネルが閉じられ、ctx.Err()関数は非nilのエラーを返します。これを利用して、キャンセルされた操作のエラーハンドリングを行うことができます。

func doSomething(ctx context.Context) error {
    // ...
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
        // ...
    }
    return nil
}

この関数は、Contextがキャンセルされた場合にはctx.Err()を返し、それ以外の場合にはnilを返します。

タイムアウトとエラーハンドリング

タイムアウトも同様に、Contextがタイムアウトした場合にはctx.Err()関数は非nilのエラーを返します。これを利用して、タイムアウトした操作のエラーハンドリングを行うことができます。

ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

err := doSomething(ctx)
if err != nil {
    fmt.Println("Error:", err)
}

このコードは、doSomething関数が100ミリ秒以内に終了しない場合にはエラーを出力します。

以上が、Go言語のcontextパッケージを使用したエラーハンドリングの基本的な方法です。これらの機能を理解し、適切に使用することで、Goの並行処理を含むコードのエラーハンドリングを効率的に行うことができます。次のセクションでは、実践的なテストケースについて説明します。

実践的なテストケース

Go言語のcontextパッケージを使用した実践的なテストケースを考えてみましょう。ここでは、HTTPリクエストを処理するサーバーのテストを例にします。

テストの準備

まず、テスト対象のサーバーを準備します。このサーバーは、指定した時間だけスリープした後にレスポンスを返すシンプルなものとします。

func server(w http.ResponseWriter, r *http.Request) {
    sleep, _ := strconv.Atoi(r.URL.Query().Get("sleep"))
    time.Sleep(time.Duration(sleep) * time.Millisecond)
    fmt.Fprintln(w, "Hello, world!")
}

タイムアウトを伴うテスト

次に、このサーバーが指定した時間内にレスポンスを返すことを確認するテストを書きます。これにはcontext.WithTimeout()を使用します。

func TestServer(t *testing.T) {
    srv := httptest.NewServer(http.HandlerFunc(server))
    defer srv.Close()

    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()

    req, _ := http.NewRequestWithContext(ctx, http.MethodGet, srv.URL+"?sleep=200", nil)
    _, err := http.DefaultClient.Do(req)

    if err == nil || !errors.Is(err, context.DeadlineExceeded) {
        t.Errorf("expected context.DeadlineExceeded, got %v", err)
    }
}

このテストでは、サーバーが200ミリ秒スリープするようにリクエストを送ります。しかし、Contextのタイムアウトは100ミリ秒に設定しているため、context.DeadlineExceededエラーが返されることを期待します。

以上が、Go言語のcontextパッケージを使用した実践的なテストケースの一例です。このように、contextパッケージは非同期操作や並行処理を含むコードのテストを効率的に行うための強力なツールとなります。この知識を活用して、品質の高いGoのコードを書くことができます。この記事があなたのGo言語の学習に役立てば幸いです。

By quonta

Related Post

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です