コンテキスト(Context) - Golang learning step 2-3
- 公開日
- カテゴリ:GoingDeeper
- タグ:Golang,roadmap.sh,学習メモ
roadmap.sh > Go > Going Deeper > Context の学習を進めていきます。
※ 学習メモとしての記録ですが、後にこのセクションを学ぶ道しるべとなるよう、ですます調で記載しています。
contents
- 開発環境
- コンテキスト(Context)
- Context とは
- Context の主な用途
- 基本的な使い方
- よく使う関数
- 実際の利用例
- Context インターフェース
- Context 使用の主なベストプラクティス
- よくある使用例
- Context を使用する際の注意点
開発環境
- チップ: Apple M2 Pro
- OS: macOS Sonoma
- go version: go1.23.2 darwin/arm64
コンテキスト(Context)
- [official] Go Context
- [article] Go by Example: Context
- [article] Digital Ocean: How to Use Contexts in Go
- [video] Context in Go
- [video] Understanding Contexts in Go
Context とは
Go の Context
は、タイムアウトやキャンセルの制御を可能にし、リクエストごとのデータをスレッドセーフに扱うために使用される仕組みです。Go のアプリケーションでは、特に並行処理やネットワークリクエストを扱う場合に Context
が用いられます。
Context の主な用途
1. キャンセルの伝搬
リクエストがキャンセルされたとき、そのキャンセルをチェーン内の全ての関数に伝搬させることができます。例えば、HTTP リクエストがタイムアウトやクライアントのキャンセルにより中断された場合、その情報を他の goroutine にも伝えることが可能です。
2. タイムアウトの設定
Context
を使用すると、タイムアウトを指定できます。これにより、指定された時間内に処理が完了しない場合に強制的に処理を終了させることができます。
3. リクエストごとのデータの伝搬
Context
には値を保存することもできるため、同一のリクエストに関連するデータを goroutine 間で共有することが可能です。ただし、Context
は小さな値や短い生存期間のものに限定して使用することが推奨されます。
基本的な使い方
context.Background()
: 通常の背景タスクで使われるデフォルトのContext
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 を使用する際の注意点
- goroutine リークの防止
Context
のキャンセルを適切に処理する- キャンセル関数は必ず呼び出す
- Context の値の型安全性
Context.Value
で値を取得する際は、型アサーションを適切に行う- キーの型は非公開にして型安全性を確保する
- Context の伝播
- 親の
Context
がキャンセルされたら、子のContext
も自動的にキャンセルされる - 新しい
Context
を作成する際は、適切な親Context
を選択する
- 親の
まとめ
Context
は、Go で並行処理やネットワークリクエストを扱う際に、キャンセルやタイムアウトの制御を可能にし、リクエストごとのデータを安全に伝播させるために使用される。context.Background()
は、通常の背景タスクで使用するデフォルトのContext
。context.WithCancel()
、context.WithTimeout()
、context.WithDeadline()
を使用することで、キャンセルやタイムアウトの制御が可能。Context
から値を取得するためにはcontext.WithValue()
を使用するが、これは小さな値に限定することが推奨されている。Context
のキャンセル関数はdefer
を使って確実に呼び出す必要がある。Context
は常に関数の最初の引数として渡し、一貫性を持たせることが重要。goroutine
リークを防ぐために、キャンセルやタイムアウトの処理は必ず行うように注意する必要があります。
[Next] Step 2-4: ゴルーチン(Goroutines)