コンテキスト(Context) - Golang learning step 2-3

公開
注意最終更新から 2 年以上経過しています。最新の情報もあわせてご確認ください。

roadmap.sh > Go > Going Deeper > Context の学習を進めていきます。

学習箇所

※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。

開発環境

  • チップ: Apple M2 Pro
  • OS: macOS Sonoma
  • go version: go1.23.2 darwin/arm64

コンテキスト(Context)

Context とは

Go の Context は、タイムアウトやキャンセルの制御を可能にし、リクエストごとのデータをスレッドセーフに扱うために使用される仕組みです。Go のアプリケーションでは、特に並行処理やネットワークリクエストを扱う場合に Context が用いられます。

Context の主な用途

1. キャンセルの伝搬

リクエストがキャンセルされたとき、そのキャンセルをチェーン内の全ての関数に伝搬させることができます。例えば、HTTP リクエストがタイムアウトやクライアントのキャンセルにより中断された場合、その情報を他の goroutine にも伝えることが可能です。

2. タイムアウトの設定

Context を使用すると、タイムアウトを指定できます。これにより、指定された時間内に処理が完了しない場合に強制的に処理を終了させることができます。

3. リクエストごとのデータの伝搬

Context には値を保存することもできるため、同一のリクエストに関連するデータを goroutine 間で共有することが可能です。ただし、Context は小さな値や短い生存期間のものに限定して使用することが推奨されます。

基本的な使い方

  1. context.Background(): 通常の背景タスクで使われるデフォルトの Context
  2. context.TODO(): 将来的に適切な Context を指定する必要があるが、現時点では未決定の場合に使用される

キャンセル可能な Context の生成

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

タイムアウト付き Context の生成

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

Context からの値の取得

Context には、キーと値のペアを渡すことができ、それを後で別の goroutine で取得できます。ただし、大量のデータを渡すことは避けるべきです。

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

よく使う関数

  • context.Background(): 最も基本的な Context。通常は最上位の Context として使用する。
  • context.WithCancel(parent Context): 親の Context にキャンセル機能を追加する。
  • context.WithTimeout(parent Context, timeout time.Duration): 一定の時間が経過すると自動的にキャンセルされる Context を作成する。
  • context.WithDeadline(parent Context, deadline time.Time): 指定された時刻までにキャンセルされる Context を作成する。
  • context.WithValue(parent Context, key, val): Context にキーと値のペアを格納する。

実際の利用例

1. キャンセル制御の例

この例では、context.WithCancel を使用し、キャンセル可能な Context を作成します。

goroutine を起動し、その goroutine 内で Context 完了のシグナルを待ち受けます。 (Context の完了は、「キャンセルされた場合(cancel() が呼ばれた)」「タイムアウトした場合」「デッドラインに達した場合」のいずれかの場合に発生)

メインの処理で 1 秒後に cancel() を呼び出すことで、goroutine にキャンセルを通知します。

package main

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

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

	go func() {
		select {
		case <-ctx.Done():
			fmt.Println("goroutine: キャンセルを検知しました")
			return
		}
	}()

	time.Sleep(1 * time.Second) // 1秒後に
	cancel()                    // キャンセルを実行
	time.Sleep(100 * time.Millisecond) // goroutineの完了を待つ
}

func main() {
	fmt.Println("=== キャンセルの例 ===")
	cancelExample()
}

// 出力:
// === キャンセルの例 ===
// goroutine: キャンセルを検知しました

2. タイムアウト制御の例

この例では、context.WithTimeout を使用し、2 秒のタイムアウトを設定します。3 秒かかる処理を実行しようとしますが、タイムアウトで中断されます。

package main

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

func timeoutExample() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	select {
	case <-time.After(3 * time.Second):
		fmt.Println("この行は実行されません")
	case <-ctx.Done():
		fmt.Println("timeout: タイムアウトしました")
	}
}

func main() {
	fmt.Println("=== タイムアウトの例 ===")
	timeoutExample()
}
// 出力:
// === タイムアウトの例 ===
// timeout: タイムアウトしました

3. 値の伝播の例

この例では、context.WithValue を使用し、userID を Context に格納します。その後、Context から値を取得し出力します。

package main

import (
	"context"
	"fmt"
)

func valueExample() {
	type key string
	const userIDKey key = "userID"

	ctx := context.WithValue(context.Background(), userIDKey, "user123")

	// 値の取得
	if userID, ok := ctx.Value(userIDKey).(string); ok {
		fmt.Printf("value: ユーザーID %s を取得しました\n", userID)
	}
}

func main() {
	fmt.Println("=== 値の伝播の例 ===")
	valueExample()
}
// 出力:
// === 値の伝播の例 ===
// value: ユーザーID user123 を取得しました

Context インターフェース

Context インターフェースは以下のメソッドを持ちます。

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Done() <-chan struct{}: このコンテキストがキャンセルされるかタイムアウトになったときに閉じられるチャネルを返す
  • Err() error: コンテキストがキャンセルされた理由を返す
  • Deadline() (deadline time.Time, ok bool): このコンテキストの期限(設定されている場合)とその有無を返す
  • Value(key interface{}): Context に紐付けられた値を返す

Context 使用の主なベストプラクティス

1. 関数の第一引数として渡す

Context を使用するプログラムは、パッケージ間でインターフェースの一貫性を保ち、静的分析ツールがコンテキストの伝播をチェックできるようにするため、Context は最初のパラメータで、通常は ctx という名前で渡します。

func DoSomething(ctx context.Context, arg string) error {
    // 処理
}

2. キャンセル関数は defer で呼び出す

確実なリソースの解放やリソースのリークを防ぐという観点から、キャンセル関数は defer で呼び出すことが推奨されています。

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

3. Context は必要以上に値を保持しない

  • Context.Value は設定値が少ない場合のみ使用
  • 大量のデータや重要な設定は別の方法で受け渡す

4. Background() と TODO() の使い分け

  • context.Background(): ルートとなる Context として使用
  • context.TODO(): Context の使用方法が明確でない場合に一時的に使用

よくある使用例

HTTP サーバーでの利用

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    // リクエストがキャンセルされた場合の処理
    select {
    case <-ctx.Done():
        return
    default:
        // 通常の処理
    }
}

データベース操作でのタイムアウト制御

func queryDatabase(ctx context.Context) error {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()
    
    return db.QueryRowContext(ctx, "SELECT ...").Scan(&result)
}

Context を使用する際の注意点

  1. goroutine リークの防止
    • Context のキャンセルを適切に処理する
    • キャンセル関数は必ず呼び出す
  2. Context の値の型安全性
    • Context.Value で値を取得する際は、型アサーションを適切に行う
    • キーの型は非公開にして型安全性を確保する
  3. Context の伝播
    • 親の Context がキャンセルされたら、子の Context も自動的にキャンセルされる
    • 新しい Context を作成する際は、適切な親 Context を選択する

まとめ

  • Context は、Go で並行処理やネットワークリクエストを扱う際に、キャンセルやタイムアウトの制御を可能にし、リクエストごとのデータを安全に伝播させるために使用される。
  • context.Background() は、通常の背景タスクで使用するデフォルトの Context。
  • context.WithCancel()、context.WithTimeout()、context.WithDeadline() を使用することで、キャンセルやタイムアウトの制御が可能。
  • Context から値を取得するためには context.WithValue() を使用するが、これは小さな値に限定することが推奨されている。
  • Context のキャンセル関数は defer を使って確実に呼び出す必要がある。
  • Context は常に関数の最初の引数として渡し、一貫性を持たせることが重要。
  • goroutine リークを防ぐために、キャンセルやタイムアウトの処理は必ず行うように注意する必要があります。
Ritolabo