Go言語のContext徹底解説：基礎から応用まで

[![Group382.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/5f41ca25-cb47-3421-7add-79f1a9c31cac.png)](https://leapcell.io/?lc_t=q_gocontext)

[Leapcell: 最適なGolangアプリホスティングのサーバレスプラットフォーム](https://leapcell.io/?lc_t=q_gocontext)

# 1. Contextとは何か？
簡単に言えば、ContextはGo 1.7バージョンで導入された標準ライブラリのインターフェースです。その定義は以下の通りです。
```go
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**：`context.Context`からキーに対応する値を取得します。これはマップの`get`メソッドと似ています。同じコンテキストで同じキーを使って`Value`を複数回呼び出すと、同じ結果が返されます。対応するキーがない場合は、`nil`を返します。キー - 値のペアは`WithValue`メソッドを通じて書き込まれます。

# 2. Contextの作成

## 2.1 ルートコンテキストの作成
ルートコンテキストを作成する主な方法は主に2つあります。
```go
context.Background()
context.TODO()
```
ソースコードを分析すると、`context.Background`と`context.TODO`の間に大きな違いはありません。どちらもルートコンテキストを作成するために使用され、機能を持たない空のコンテキストです。ただし、一般的に、現在の関数にコンテキストが入力パラメータとして存在しない場合、通常は`context.Background`を使用してルートコンテキストを作成し、開始コンテキストとして下に渡します。

## 2.2 子コンテキストの作成
ルートコンテキストが作成された後、それ自体には機能はありません。コンテキストをプログラムで役立たせるには、`context`パッケージが提供する`With`シリーズの関数を使って派生させます。
主に以下の派生関数があります。
```go
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つの`With`シリーズのメソッドを使って4種類のコンテキストを派生させることができます。各コンテキストは同じ方法で`With`シリーズのメソッドを呼び出すことで、新しい子コンテキストを引き続き派生させることができ、全体の構造が木のように見えます。

# 3. Contextの用途は何か？
Contextは主に2つの用途があり、これらはまたプロジェクトでも一般的に使用されます。
- 並行制御のため、goroutineを優雅に終了させるため。
- コンテキスト情報を渡すため。

一般的に、Contextは親と子のgoroutine間で値を渡し、キャンセル信号を送信するメカニズムです。

## 3.1 並行制御
典型的なサーバーは継続的に実行され、クライアントやブラウザからの要求を受け取り、応答するように待機しています。このシナリオを考えてみましょう：バックエンドのマイクロサービスアーキテクチャで、サーバーが要求を受け取った場合、ロジックが複雑な場合は、単一のgoroutineでタスクを完了することはできません。代わりに、多くのgoroutineを作成して一緒に要求を処理します。要求が入力されると、まずRPC1呼び出しを経て、次にRPC2に進み、さらに2つのRPCが作成されて実行されます。RPC4の中には別のRPC呼び出し（RPC5）があります。すべてのRPC呼び出しが成功すると結果が返されます。この一連の呼び出しの過程で、もしRPC1でエラーが発生したとしましょう。コンテキストがない場合、すべてのRPCが終了するまで待たなければならず、実際には多くの時間が浪費されます。なぜなら、エラーが発生するとすぐに、RPC1で結果を返し、後続のRPCが完了するのを待つ必要がないからです。もしRPC1で失敗を直接返し、後続のRPCが続行するのを待たない場合、後続のRPCの実行は実際には意味がなく、単に計算資源とI/O資源を浪費するだけです。コンテキストを導入することで、この問題をうまく処理することができます。子goroutineがもはや必要ない場合、コンテキストを通じてそれらに優雅に終了するよう通知することができます。

### 3.1.1 context.WithCancel
このメソッドは以下のように定義されています。
```go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
```
`context.WithCancel`関数はキャンセル制御関数です。コンテキストを1つのパラメータとして取り、`context.Context`から新しい子コンテキストとキャンセル関数`CancelFunc`を派生させることができます。この子コンテキストを新しいgoroutineに渡すことで、これらのgoroutineの終了を制御することができます。返されたキャンセル関数`CancelFunc`を実行すると、現在のコンテキストとその子コンテキストがキャンセルされ、すべてのgoroutineがキャンセル信号を同期的に受け取ります。
使用例：
```go
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)   // goroutine1とgoroutine2を6秒間実行させる
   fmt.Println("end working!!!")
   cancel()  // goroutine1とgoroutine2に終了を通知する
   time.Sleep(1 * time.Second)
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // メインのgoroutineがcancelを呼び出した後、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`を派生させ、子goroutineに渡します。次の6秒間は、`cancel`関数が実行されないため、子goroutineは常に`default`文を実行し、監視情報を印刷します。6秒後に`cancel`が呼び出されます。この時、子goroutineは`ctx.Done()`チャネルからメッセージを受け取り、`return`を実行して終了します。

### 3.1.2 context.WithDeadline
このメソッドは以下のように定義されています。
```go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
```
`context.WithDeadline`もキャンセル制御関数です。このメソッドは2つのパラメータを持ちます。最初のパラメータはコンテキストで、2番目のパラメータは期限です。また、子コンテキストとキャンセル関数`CancelFunc`を返します。使用する際、期限までに、手動で`CancelFunc`を呼び出して子コンテキストをキャンセルし、子goroutineの終了を制御することができます。期限までに`CancelFunc`を呼び出さなかった場合、子コンテキストの`Done()`チャネルにもキャンセル信号が届き、子goroutineの終了を制御します。
使用例：
```go
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)   // goroutine1とgoroutine2を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秒後に子goroutine内の`ctx.Done()`が信号を受け取り、`exit`を印刷し、子goroutineが終了しました。これが`WithDeadline`を使って子コンテキストを派生させる方法です。

### 3.1.3 context.WithTimeout
このメソッドは以下のように定義されています。
```go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
```
`context.WithTimeout`は機能的に`context.WithDeadline`と似ています。どちらもタイムアウトによって子コンテキストをキャンセルするために使用されます。唯一の違いは渡される2番目のパラメータです。`context.WithTimeout`が渡す2番目のパラメータは特定の時刻ではなく、期間です。
使用例：
```go
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)   // goroutine1とgoroutine2を6秒間実行させる
   fmt.Println("end working!!!")
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // メインのgoroutineがcancelを呼び出した後、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.WithDeadline`の例のコードと同じで、ただコンテキストを派生させるメソッドが`context.WithTimeout`に変更されています。具体的には、2番目のパラメータはもはや特定の時刻ではなく、4秒という特定の期間になっています。実行結果も同じです。

### 3.1.4 context.WithValue
このメソッドは以下のように定義されています。
```go
func WithValue(parent Context, key, val interface{}) Context
```
`context.WithValue`関数は、親コンテキストから子コンテキストを作成し、値を渡すために使用されます。関数のパラメータは、親コンテキストとキー - 値のペア（key, val）です。コンテキストを返します。プロジェクトでは、このメソッドは一般的に、一意の要求IDやトレースIDなどのコンテキスト情報を渡すために使用され、リンクトレーシングや設定の渡しに役立ちます。
使用例：
```go
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アプリホスティングのサーバレスプラットフォーム](https://leapcell.io/?lc_t=q_gocontext)

[![barndpic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/9b9bcfc6-16bd-ea6b-11bb-cbcc790a6baf.png)](https://leapcell.io/?lc_t=q_gocontext)



最後に、Golangサービスのデプロイに最適なプラットフォームを1つおすすめします：[Leapcell](https://leapcell.io/)

### 1. 多言語対応
 - JavaScript、Python、Go、またはRustで開発できます。

### 2. 無制限のプロジェクトを無料でデプロイ
 - 使用量に応じて課金 — 要求がなければ料金はかかりません。

### 3. 圧倒的なコスト効率
 - 使った分だけ課金で、アイドル時の料金はかかりません。
 - 例：25ドルで平均応答時間60msで694万回の要求をサポートできます。

### 4. ストリームライン化された開発者体験
 - 直感的なUIで簡単にセットアップできます。
 - 完全自動化されたCI/CDパイプラインとGitOpsの統合。
 - アクション可能な洞察のためのリアルタイムメトリックとログ。

### 5. 簡単なスケーラビリティと高パフォーマンス
 - 高い並行処理を簡単に処理できる自動スケーリング。
 - オペレーション上のオーバーヘッドはゼロ — 構築に集中できます。

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/10307af9-b27d-b341-13e2-4c7388f264e9.png)](https://leapcell.io/?lc_t=q_gocontext)


[ドキュメントで詳細を確認！](https://docs.leapcell.io/)

LeapcellのTwitter：[https://x.com/LeapcellHQ](https://x.com/LeapcellHQ)