> Source: https://www.ymotongpoo.com/works/goblog-ja/context-and-structs/


# コンテキストと構造体
[Contexts and structs](https://go.dev/blog/context-and-structs) by Jean Barkhuysen, Matt T. Proud

## はじめに

多くのGoのAPI、特に現代的なAPIでは、関数やメソッドの最初の引数として
[`context.Context`](/pkg/context/) を取ることがよくあります。コンテキストは、
デッドラインや呼び出し元によるキャンセル、その他リクエストスコープの値をAPIの境界を越えて、
またプロセス間で伝達する手段を提供します。これはライブラリがデータベースやAPIなど、
直接的あるいは間接的にリモートサーバーとやり取りする場合によく使われます。

[contextパッケージのドキュメント](/pkg/context/) には次のように書かれています。

> Contexts should not be stored inside a struct type, but instead passed to each function that needs it.
>
> （コンテキストは構造体型の中に格納すべきではなく、それを必要とする各関数に渡すべきです。）

この記事では、なぜコンテキストを他の型に格納するのではなく引数として渡すことが重要なのか、
その理由と例を挙げてこのアドバイスを詳しく説明します。またコンテキストを構造体型に格納することが
理にかなっている稀なケースについても触れ、その安全な実現方法を紹介します。

## 引数として渡すコンテキストを優先する

コンテキストを構造体に格納すべきではないというアドバイスを理解するために、まずは好ましいとされる
「コンテキストを引数として渡す」アプローチを見てみましょう。

```go
// Worker はリモートの作業オーケストレーションサーバーに対して作業を取得し、追加します。
type Worker struct { /* … */ }

type Work struct { /* … */ }

func New() *Worker {
  return &Worker{}
}

func (w *Worker) Fetch(ctx context.Context) (*Work, error) {
  _ = ctx // 呼び出しごとのctxはキャンセル、デッドライン、メタデータのために使われます。
}

func (w *Worker) Process(ctx context.Context, work *Work) error {
  _ = ctx // 呼び出しごとのctxはキャンセル、デッドライン、メタデータのために使われます。
}
```

ここでは `(*Worker).Fetch` と `(*Worker).Process` の両方のメソッドが、コンテキストを直接引数として受け取っています。
この「引数として渡す」設計により、利用者は呼び出しごとにデッドラインやキャンセル、メタデータを設定できます。
また、各メソッドに渡された `context.Context` がどう使われるかも明確です。あるメソッドに渡した
`context.Context` が他のメソッドで使われるという期待は一切ありません。これはコンテキストが必要最小限の
操作の範囲にスコープされているためであり、このパッケージにおける `context` の有用性と明確さを大きく高めています。

## コンテキストの構造体格納が招く混乱

先ほどの `Worker` の例を、今度は好ましくない「コンテキストを構造体に格納する」アプローチで見直してみましょう。
この方法の問題点は、コンテキストを構造体に格納すると、そのライフタイムが呼び出し元から見えにくくなり、
さらに悪いことに2つのスコープが予測できない形で入り混じってしまうことです。

```go
type Worker struct {
  ctx context.Context
}

func New(ctx context.Context) *Worker {
  return &Worker{ctx: ctx}
}

func (w *Worker) Fetch() (*Work, error) {
  _ = w.ctx // 共有されたw.ctxがキャンセル、デッドライン、メタデータのために使われます。
}

func (w *Worker) Process(work *Work) error {
  _ = w.ctx // 共有されたw.ctxがキャンセル、デッドライン、メタデータのために使われます。
}
```

`(*Worker).Fetch` と `(*Worker).Process` の両方のメソッドは、Workerに格納されたコンテキストを使っています。
これでは、FetchとProcessの呼び出し元（それぞれ異なるコンテキストを持っているかもしれません）が、
呼び出しごとにデッドラインを指定したり、キャンセルを要求したり、メタデータを付与したりできません。
たとえば、利用者は `(*Worker).Fetch` だけにデッドラインを設定したり、 `(*Worker).Process` の呼び出しだけを
キャンセルしたりできません。呼び出し元のライフタイムは共有されたコンテキストと混ざり合ってしまい、
コンテキストのスコープは `Worker` が生成された時点のライフタイムに固定されてしまいます。

このAPIは、引数として渡すアプローチと比べても、利用者にとってはるかにわかりにくいものになります。
利用者は次のような疑問を持つかもしれません。

* `New` が `context.Context` を受け取るということは、コンストラクタがキャンセルやデッドラインを必要とする
  処理を行っているということなのだろうか？
* `New` に渡された `context.Context` は `(*Worker).Fetch` や `(*Worker).Process` の処理に適用されるのだろうか？
  どちらにも適用されない？それとも片方だけ？

このAPIでは、 `context.Context` が正確に何のために使われるのかを利用者に明示的に伝えるために、
かなりの量のドキュメントが必要になるでしょう。また利用者は、APIの構造が伝えてくれる情報に頼るのではなく、
コードを読まなければならなくなるかもしれません。

そして最後に、リクエストのそれぞれがコンテキストを持たずキャンセルを適切に扱えないような本番グレードの
サーバーを設計することは危険です。呼び出しごとのデッドラインを設定できなければ、
[プロセスに処理が滞留し](https://sre.google/sre-book/handling-overload/)、
メモリなどのリソースを枯渇させてしまう可能性があります。

## 後方互換性を維持するための例外

Go 1.7（[context.Contextを導入した](/doc/go1.7)バージョン）がリリースされたとき、
多くのAPIが後方互換性を保ったままコンテキストのサポートを追加しなければなりませんでした。たとえば
[`net/http` の `Client` のメソッド](/pkg/net/http/)である `Get` や `Do` は、コンテキストを導入する
絶好の候補でした。これらのメソッドで送信される外部へのリクエストはそれぞれ、 `context.Context` が
もたらすデッドライン、キャンセル、メタデータのサポートの恩恵を受けられるはずでした。

後方互換性を保ちながら `context.Context` のサポートを追加する方法は2つあります。一つはこれから見ていく、
構造体にコンテキストを含める方法、もう一つは関数を複製し、複製したほうに `context.Context` を受け取らせて
関数名の末尾に `Context` を付ける方法です。後者の複製する方法のほうが、構造体にコンテキストを含める方法よりも
好ましく、これについては [Keeping your modules compatible](/blog/module-compatibility) でさらに詳しく
議論されています。しかし、場合によってはこれが現実的でないこともあります。たとえば、あなたのAPIが
多くの関数を公開している場合、それらすべてを複製するのは実行不可能かもしれません。

`net/http` パッケージは構造体にコンテキストを含めるアプローチを選択しており、これは有用な事例研究となります。
`net/http` の `Do` を見てみましょう。 `context.Context` が導入される前、 `Do` は次のように定義されていました。

```go
// Do は HTTP リクエストを送信し、HTTP レスポンスを返します […]
func (c *Client) Do(req *Request) (*Response, error)
```

Go 1.7以降、もし後方互換性を壊すことにならないのであれば、 `Do` は次のようになっていたかもしれません。

```go
// Do は HTTP リクエストを送信し、HTTP レスポンスを返します […]
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
```

しかし、標準ライブラリにとっては後方互換性を維持し、
[Go 1における互換性の約束](/doc/go1compat) を守ることが重要です。そこでメンテナーたちは代わりに、
後方互換性を壊すことなく `context.Context` のサポートを可能にするために、 `http.Request` 構造体に
`context.Context` を追加するという選択をしました。

```go
// Request はサーバーが受信する、あるいはクライアントが送信するHTTPリクエストを表します。
// ...
type Request struct {
  ctx context.Context

  // ...
}

// NewRequestWithContext はメソッド、URL、任意のボディを受け取り、新しいRequestを返します。
// [...]
// 与えられたctxはRequestのライフタイムを通して使われます。
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
  // この記事のために簡略化しています。
  return &Request{
    ctx: ctx,
    // ...
  }
}

// Do は HTTP リクエストを送信し、HTTP レスポンスを返します […]
func (c *Client) Do(req *Request) (*Response, error)
```

自分のAPIに後付けでコンテキストのサポートを組み込む際には、上記のように構造体に `context.Context` を
追加することが理にかなっている場合もあります。しかしその前に、まず関数を複製する方法を検討することを
忘れないでください。この方法であれば、有用性や理解しやすさを犠牲にすることなく、後方互換性を保ったまま
`context.Context` を後付けできます。たとえば次のようになります。

```go
// Call は内部でcontext.Backgroundを使います。コンテキストを指定したい場合は
// CallContextを使ってください。
func (c *Client) Call() error {
  return c.CallContext(context.Background())
}

func (c *Client) CallContext(ctx context.Context) error {
  // ...
}
```

## 結論

コンテキストを使えば、ライブラリやAPIをまたいだ重要な情報を呼び出しスタックの下流へと伝播させるのが
簡単になります。しかし理解しやすく、デバッグしやすく、効果的であり続けるためには、一貫して明確に
使わなければなりません。

構造体に格納するのではなくメソッドの最初の引数として渡すことで、利用者はコンテキストの拡張性を最大限に
活用し、呼び出しスタックを通じてキャンセルやデッドライン、メタデータの情報からなる強力な木構造を
構築できます。そして何より、引数として渡された場合はそのスコープが明確に理解できるため、
スタックの上流でも下流でも明快な理解とデバッグのしやすさにつながります。

コンテキストを使ったAPIを設計するときは、次のアドバイスを忘れないでください。 `context.Context` は
引数として渡すこと。構造体に格納しないこと。

## 参考文献

* [Go Concurrency Patterns: Context (2014 blog post)](https://blog.golang.org/context)
  * [Goの並行パターン：コンテキスト (Go Concurrency Pattern: Context)](../context/)

By Jean Barkhuysen, Matt T. Proud

