GOルーチンの非同期処理で context の取り扱いに気をつけないと事故る

`Go`の`context`パッケージの使い方がイマイチよく分からない。このように思っている方は、多いのではないでしょうか。

「**なんかリクエスト情報を持っている**」、今までなんとなくそう思いながら使っていました。そして、その`context`の使い方を間違えて本番にリリースした結果・・・　見事に事故りました。

## 事故の経緯
今回使用したのは、`graphql`というライブラリです。GraphQLクエリ実行に便利なクライアントですね。
https://github.com/machinebox/graphql

社内のAPIからコールしていたのは、このライブラリの`graphql.Run()`メソッドです。今回の修正がこちら。
もともと`context.Backgrount()`で空の`context`を生成して使用していたのですが、同じリクエストスコープだからといって、パラメータの`context`を使用することにしたら、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272102/a360123b-b508-3ab4-7e41-91f019c59685.png)
・
・
・
事故りました−−−
`client.Run()`でエラーが連発していたんです（泣）

## 事故の原因
`client.Run()`の中身を覗いてみると、先頭で[このようなチェック](https://github.com/machinebox/graphql/blob/master/graphql.go#L84-L89)をしています。
```go
func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error {
	select {
	case <-ctx.Done():
		return ctx.Err()
	default:
	}
	if len(req.files) > 0 && !c.useMultipartForm {
```

パラメータの`context`が終了しているか否かを、`ctx.Done()`でチェックしているわけです。「**先程まで使っていた`context`だから、終了しているわけないじゃん**」と思ったあなた。実はここに大きな落とし穴がありました。誰も気づかない大きな大きな落とし穴が・・・

この`graphql`処理を呼んでいる関数`doSomething()`ですが、呼び出し元ではこんな呼び方をしていました。
```go
func do(ctx context.Context) {
  go doSomething(ctx)
}
```
`Goroutine`で呼び出していたわけです。

つまり、先程まで`context`が生きていたからといって、違う`Goroutine`でも生きている保証はないんです。なぜかというと、`waitGroup`も何も使ってなく、ただ別の`Goroutine`で呼び出しているだけなので、呼び出し元の`Goroutine`が待ってくれる保証がないからです。

呼び出し元の親`Goroutine`が先に終了した場合、子`Goroutine`内では`ctx<-Done()`になってしまい、エラーを返してくるわけです。

## ここまでのまとめ
親子間で異なる`context`を使った場合は、何の心配も不要です。ご自由にご利用ください。

以下、親子間で同じ`context`を使いまわした場合の挙動をまとめます。
- 同期処理の場合
  - 親`Goroutine`が子`Goroutine`処理を待ってくれるので、`context`を使いまわしても問題ない
- 非同期処理の場合
  - 親`Goroutine`が子`Goroutine`処理を待ってくれる保証はないので、親`Goroutine`が先に終了すると、子`Goroutine`内の`ctx<-Done()`になる。もちろんその次の挙動は実装によります。上記の例では、`ctx.Err()`を返してくれていたので、エラーになったわけです。

## 同じ`context`を使い回す意味
「**じゃ、毎回違う`context`を使ったらいいじゃん**」と思うかもしれませんが、一度原点に戻って考えてみます。そもそも`context`は、**プロセス間を横断する必要のあるリクエストスコープな値を伝達させる**ためのものです。
>Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
>
>（訳）パッケージコンテキストはコンテキストタイプを定義します。コンテキストタイプは、APIの境界を越えて、プロセス間で、期限、キャンセルシグナル、およびその他のリクエストスコープの値を伝達します。
出典：[pkg.go.dev - context pkg](https://pkg.go.dev/context#pkg-overview)

今回はトレースログを収集するために、同じ`context`を使い回すことが必要でした。親子間で異なる`context`を使ったら、途中でトレースが追えなくなってしまいます。

## 考えられる対応策
元の`context`の値を引き継いだ、新しい`context`を生成することが求められます。実際`context`パッケージ内には、次のようなメソッドが提供されています。いずれも元の`context`の値を引き継ぎながら、オプション的な機能が追加された新しい`context`が生成されます。
```go:context.go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val any) Context
```

しかし、今回は元の`context`とまったく同じ`context`を生成するだけで物足ります。オプション的な機能は必要ありません。既存のメソッドを利用すると、使用しない機能が追加された（ゴミデータが混ざっている）`context`が生成されるので、使用を躊躇しちゃいました。

ということでたどり着いたのが、次に示す方法です。

## 採用した対応策
元の`context`の値のみをコピーした、新しい`context`を生成して使用することにしました。公式の`context`パッケージを眺めてみると、`Context`の定義は次のようになっています。
```go:context.go
type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
```

公式の`context`を内包した構造体を作り、上記`interface`を実装すれば行けそうな気がします。そして、実際実装した`customｃontext`の中身がこちらです。
```go:customcontext.go
package customcontext

import (
	"context"
	"time"
)

type customContext struct {	ctx context.Context	}

func (customContext) Deadline() (time.Time, bool) { return time.Time{}, false }
func (customContext) Done() <-chan struct{} { return nil }
func (customContext) Err() error { return nil }
func (x customContext) Value(key any) any { return x.ctx.Value(key) }

func Clone(ctx context.Context) context.Context { return customContext{ctx} }

```

使用する箇所では`customcontext.Clone()`メソッドで、値がコピーされた新しい`context`を作って使用するだけです。これで、元の`context`の値を引き継いでおり、且つ元の`context`とは生存関係を持たない新しい`context`が作れました。
めでたし❣　めでたし❣
```go
func do(ctx context.Context) {
  newCtx := customcontext.Clone(ctx)
  go doSomething(newCtx)
}
```

## 参考資料
- [よく分かるcontextの使い方](https://zenn.dev/hsaki/books/golang-context/viewer/intro)
- [context.Context の Value のみを引き継いでコピーする方法](https://zenn.dev/knwoop/articles/dd93fffb48775b)