概要
go 1.7からcontextパッケージが標準パッケージになりました。
タイムアウト、キャンセルなどのハンドリングができることから、ブロッキングする処理や外部APIリクエストなどを扱う時は基本的に第一引数に置くべきです。
またAPIやプロセス間通信のリクエストスコープの値を引き継がせる際にも利用されます。
例えばgoogleはAPIを叩くコードでは全ての関数にcontextが引数に存在します。
ブログでもこのように言っています。
At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests.
ref: Go Concurrency Patterns: Context - go.dev
一方でcontext.Value
になんでも詰め込めるため、汎用的な関数のインタフェースの引数として使うのは?という考え方もあります。
type Hoge interface { DoSomething(ctx context.Context) }
こんな感じですね。引数になんでも詰め込めるので、簡単に共通のインタフェースを持たせることができます。
結論から言うとこの使い方は良くないです。今回はcontext.Contextの使う基準について考えてみます。
環境
- golang 1.10
主な方針
主に以下の2つの時に使うと良いです。
- キャンセルのためのシグナルの受け渡し
- リクエストスコープの値を引き継がせるため
a. キャンセルのためのシグナルの受け渡し
外部APIであったり、時間がかかる処理は基本的にキャンセル可能にすべきです。
contextはタイムアウトとしてや、メソッド呼び出しでキャンセルを発火できるので、例えばgoroutineで親の処理が終わった時に子のgoroutineを強制的に終わらせる、といったことができます。
例えばこちらでも指摘していますが、あるgoroutineAでエラーが起きたので親にそれが伝播して親もerrorを返して終了した、が、他のgoroutineB〜は生きてるみたいなケースが起きないよう、他のgoroutineもcontextを渡してctx.Done()
で終了するような実装にしておく時にcontextが使えます。
b. リクエストスコープの値を引き継がせるため
例えばリクエストに付加される以下のような情報を付けるのはOKです。
- userID
- tokenなど認証情報
- loggerに出力させたい情報(IP、user-agentなど)
また付加する情報は不変であるべきで、途中で変化する状態のような値は保持すべきでないです。
なぜなら引き渡された関数では、そのcontextが持つ状態が変更されているのかどうなのかが分からないからです。
そのためある場所では値が変わっていて期待する挙動も変わってしまったといったことが起き、バグの温床となります。
最初は「ここだけだから」という気持ちで誰かが値を変更させてしまうと、他の開発者も「じゃあここもいいよね」となり、最終的には全コードを把握しないといけないことになるので最初から入れないほうが良いでしょう。
汎用的な関数のインタフェースの引数として使うのは?は何故駄目なのか
そもそもですが、ドキュメントには
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
ref: context package - context - pkg.go.dev
とあるように、API通信やプロセス間通信でのリクエストスコープの値の継承でのみ使うべきと書いてあります。
冒頭のGoogleの方針も
At Google, we require that Go programmers pass a Context parameter as the first argument to every function on the call path between incoming and outgoing requests.
とあるように、on the call path between incoming and outgoing requests.
と、このときだけです。
そしてそうでないケースで使用すると、以下のデメリットが発生します。
- contextはロジックを分かりにくくする
- 値に何が入っているか不明なので、リファクタする時にメンテコストが非常に大きい
- 型安全性が失われる
contextのそもそもの思想や、こういったデメリットがあることから通常の関数の引数には付けるべきではないです。
まとめ
contextを使う時は
- キャンセルのためのシグナルの受け渡しとして
- リクエストスコープの値を引き継がせるため
の2つに当てはまるかどうかで判断するといいです。