Golang Context Deep Dive: From Zero to Hero
Olivia Novak
Dev Intern · Leapcell

1. Contextとは?
簡単に言うと、ContextはGoバージョン1.7で導入された標準ライブラリのインターフェースです。その定義は以下の通りです。
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
このインターフェースは4つのメソッドを定義しています。
- Deadline:
context.Context
がキャンセルされる時間、つまり締め切りを設定します。 - Done: 読み取り専用のチャネルを返します。Contextがキャンセルされるか、締め切りに達すると、このチャネルは閉じられ、Contextのチェーンの終わりを示します。
Done
メソッドへの複数回の呼び出しは、同じチャネルを返します。 - Err:
context.Context
の終了理由を返します。Done
によって返されるチャネルが閉じられた場合にのみ、null以外の値を返します。戻り値には2つのケースがあります。context.Context
がキャンセルされた場合、Canceled
を返します。context.Context
がタイムアウトした場合、DeadlineExceeded
を返します。
- Value: マップの
get
メソッドと同様に、context.Context
からキーに対応する値を取得します。同じコンテキストの場合、同じキーでValue
を複数回呼び出すと、同じ結果が返されます。対応するキーがない場合は、nil
を返します。キーと値のペアは、WithValue
メソッドを通じて書き込まれます。
2. Contextの作成
2.1 ルートコンテキストの作成
ルートコンテキストを作成するには、主に2つの方法があります。
context.Background() context.TODO()
ソースコードを分析すると、context.Background
とcontext.TODO
にはあまり違いはありません。どちらもルートコンテキストを作成するために使用され、これは機能を持たない空のコンテキストです。ただし、一般的に、現在の関数に入力パラメータとしてコンテキストがない場合、通常はcontext.Background
を使用してルートコンテキストを作成し、それを開始コンテキストとして渡します。
2.2 子コンテキストの作成
ルートコンテキストが作成された後、それには機能がありません。コンテキストをプログラムで役立つものにするには、context
パッケージによって提供されるWith
シリーズの関数に依存して派生させます。
主に以下の派生関数があります。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) func WithValue(parent Context, key, val interface{}) Context
現在のコンテキストに基づいて、各With
関数は新しいコンテキストを作成します。これは、おなじみのツリー構造に似ています。現在のコンテキストは親コンテキストと呼ばれ、新しく派生したコンテキストは子コンテキストと呼ばれます。ルートコンテキストを通じて、4種類のコンテキストは、4つのWith
シリーズメソッドを使用して派生させることができます。各コンテキストは、同じ方法でWith
シリーズメソッドを呼び出すことによって、新しい子コンテキストを引き続き派生させることができ、全体的な構造はツリーのように見えます。
3. Contextの用途とは?
Contextには主に2つの用途があり、これはプロジェクトでも一般的に使用されます。
- 並行制御のために、ゴルーチンを正常に終了させる。
- コンテキスト情報を渡すため。
一般に、Contextは、親ゴルーチンと子ゴルーチンの間で値を渡し、キャンセル信号を送信するメカニズムです。
3.1 並行制御
一般的なサーバーの場合、クライアントまたはブラウザからリクエストを受信して応答するのを待機しながら、継続的に実行されます。バックエンドマイクロサービスアーキテクチャで、サーバーがリクエストを受信したときに、ロジックが複雑な場合、単一のゴルーチンでタスクを完了することはありません。代わりに、多くのゴルーチンを作成して連携してリクエストを処理します。リクエストが到着すると、最初にRPC1呼び出しを経由し、次にRPC2に移動し、さらに2つのRPCが作成されて実行されます。RPC4内には別のRPC呼び出し(RPC5)があります。すべてのRPC呼び出しが成功すると、結果が返されます。呼び出しプロセス全体でRPC1でエラーが発生したとします。コンテキストがない場合、すべてのRPCが完了するまで待ってから結果を返す必要があり、実際には多くの時間を浪費します。エラーが発生すると、後続のRPCが完了するのを待たずに、RPC1で直接結果を返すことができるためです。RPC1で直接失敗を返し、後続のRPCが続行するのを待たない場合、後続のRPCの実行は実際には無意味であり、計算およびI/Oリソースを浪費するだけです。コンテキストを導入した後、この問題をうまく処理できます。子ゴルーチンが不要になったら、コンテキストを通じて正常に閉じるように通知できます。
3.1.1 context.WithCancel
このメソッドは次のように定義されています。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
context.WithCancel
関数は、キャンセル制御関数です。コンテキストをパラメータとしてのみ受け取り、新しい子コンテキストとキャンセル関数CancelFunc
をcontext.Context
から導出できます。この子コンテキストを新しいゴルーチンに渡すことで、これらのゴルーチンのクローズを制御できます。返されたキャンセル関数CancelFunc
を実行すると、現在のコンテキストとその子コンテキストがキャンセルされ、すべてのゴルーチンがキャンセル信号を同期的に受信します。
使用例:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithCancel(context.Background()) go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // ゴルーチン1とゴルーチン2を6秒間実行させる fmt.Println("end working!!!") cancel() // ゴルーチン1とゴルーチン2に閉じるように通知する time.Sleep(1 * time.Second) } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // メインゴルーチンがキャンセルを呼び出した後、信号がctx.Done()チャネルに送信され、この部分がメッセージを受信する return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
実行結果:
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
end working!!!
goroutine1 exit!
goroutine2 exit!
ctx, cancel := context.WithCancel(context.Background())
は、戻り関数cancel
を持つctx
を派生させ、それを子ゴルーチンに渡します。次の6秒間、cancel
関数が実行されないため、子ゴルーチンは常にdefault
ステートメントを実行し、監視情報を出力します。6秒後、cancel
が呼び出されます。この時点で、子ゴルーチンはctx.Done()
チャネルからメッセージを受信し、return
を実行して終了します。
3.1.2 context.WithDeadline
このメソッドは次のように定義されています。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context.WithDeadline
もキャンセル制御関数です。このメソッドには2つのパラメータがあります。最初のパラメータはコンテキスト、2番目のパラメータは締め切りです。また、子コンテキストとキャンセル関数CancelFunc
を返します。これを使用する場合、締め切り前に、CancelFunc
を手動で呼び出して子コンテキストをキャンセルし、子ゴルーチンの終了を制御できます。締め切りまでにCancelFunc
を呼び出していない場合でも、子コンテキストのDone()
チャネルはキャンセル信号を受信して、子ゴルーチンの終了を制御します。
使用例:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(4*time.Second)) // タイムアウトを現在時刻から4秒に設定する defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // ゴルーチン1とゴルーチン2を6秒間実行させる fmt.Println("end working!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // 4秒後に信号を受信する return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
実行結果:
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!
cancel
関数を呼び出しませんでしたが、4秒後、子ゴルーチンのctx.Done()
が信号を受信し、exit
と出力して、子ゴルーチンが終了しました。これが、WithDeadline
を使用して子コンテキストを派生させる方法です。
3.1.3 context.WithTimeout
このメソッドは次のように定義されています。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
context.WithTimeout
は、機能においてcontext.WithDeadline
と似ています。どちらもタイムアウトによる子コンテキストのキャンセルに使用されます。唯一の違いは、渡される2番目のパラメータにあります。context.WithTimeout
によって渡される2番目のパラメータは、特定の日時ではなく、期間です。
使用例:
package main import ( "context" "fmt" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() go Watch(ctx, "goroutine1") go Watch(ctx, "goroutine2") time.Sleep(6 * time.Second) // ゴルーチン1とゴルーチン2を6秒間実行させる fmt.Println("end working!!!") } func Watch(ctx context.Context, name string) { for { select { case <-ctx.Done(): fmt.Printf("%s exit!\n", name) // メインゴルーチンがキャンセルを呼び出した後、信号がctx.Done()チャネルに送信され、この部分がメッセージを受信する return default: fmt.Printf("%s working...\n", name) time.Sleep(time.Second) } } }
実行結果:
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine2 working...
goroutine1 working...
goroutine1 working...
goroutine2 working...
goroutine1 exit!
goroutine2 exit!
end working!!!
このプログラムは非常に簡単です。コンテキストを派生させるメソッドがcontext.WithTimeout
に変更されている点を除いて、以前のcontext.WithDeadline
のサンプルコードと基本的に同じです。具体的には、2番目のパラメータは特定の日時ではなく、4秒という特定の期間になりました。実行結果も同じです。
3.1.4 context.WithValue
このメソッドは次のように定義されています。
func WithValue(parent Context, key, val interface{}) Context
context.WithValue
関数は、値の受け渡しのために親コンテキストから子コンテキストを作成します。関数のパラメータは、親コンテキスト、キーと値のペア(key、val)です。これによりコンテキストが返されます。プロジェクトでは、このメソッドは通常、リンクの追跡や構成の受け渡しのために、一意のリクエストIDやトレースIDなどのコンテキスト情報を渡すために使用されます。
使用例:
package main import ( "context" "fmt" "time" ) func func1(ctx context.Context) { fmt.Printf("name is: %s", ctx.Value("name").(string)) } func main() { ctx := context.WithValue(context.Background(), "name", "leapcell") go func1(ctx) time.Sleep(time.Second) }
実行結果:
name is: leapcell
Leapcell:Golangアプリのホスティングに最適なサーバーレスプラットフォーム
最後に、Golangサービスのデプロイに最適なプラットフォームをお勧めします。Leapcell
1. 多言語サポート
- JavaScript、Python、Go、またはRustで開発します。
2. 無制限のプロジェクトを無料でデプロイ
- 使用量に対してのみ支払い - リクエストも料金もかかりません。
3. 比類のない費用対効果
- アイドル料金なしで従量課金制。
- 例:$25で、平均応答時間60msで694万件のリクエストをサポートします。
4. 合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOpsの統合。
- 実用的な洞察を得るためのリアルタイムのメトリックとロギング。
5. 簡単なスケーラビリティと高性能
- 高い並行性を容易に処理するための自動スケーリング。
- 運用上のオーバーヘッドはゼロ - 構築に集中するだけです。
Leapcell Twitter: https://x.com/LeapcellHQ