Goのcontext.WithValue乱用で依存が見えない実装をどうレビューするか
Goのcontext.WithValue乱用で依存が見えない実装をどうレビューするか
Goの context.Context は便利だ。
リクエストID、認証主体、トレース情報などを下流へ渡せるので、実装は一見すっきり見える。
ただし、業務パラメータまで context.WithValue に押し込み始めた実装は、
レビューでかなり早い段階で止めたほうがよい。
典型的には次のような問題が起きる。
- 関数シグネチャから必要な入力が読めなくなる
- string key ベースで型安全性が崩れる
- ハンドラ都合の値受け渡しがサービス層へ漏れる
- nil や型アサーション失敗が障害時だけ露出する
この記事では WithValue を全面否定するのではなく、
どこまでが許容範囲で、どこから差し戻すべきかをレビュー観点で整理する。
まず止めたい実装
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), "userID", r.Header.Get("X-User-ID"))
ctx = context.WithValue(ctx, "currency", r.URL.Query().Get("currency"))
ctx = context.WithValue(ctx, "dryRun", r.URL.Query().Get("dryRun") == "true")
@Reviewer業務パラメータが `context.Value()` に隠れており、関数シグネチャから必要入力が読めません。contextは制御情報に寄せ、業務入力は明示引数や構造体に切り出してください。
if err := createOrder(ctx); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func createOrder(ctx context.Context) error {
userID := ctx.Value("userID").(string)
currency := ctx.Value("currency").(string)
dryRun := ctx.Value("dryRun").(bool)
@Reviewerstring key と型アサーションに依存しており、欠落時にpanicまたは実行時エラーになります。呼び出し契約が静的に保証されない構造です。
if dryRun {
return nil
}
return saveOrder(ctx, userID, currency)
}このコードは、引数が少なく見えるぶん一見きれいに見える。
だが実際には、依存を見えない場所へ押し込んでいるだけである。
なぜ危ないのか
createOrder(ctx) というシグネチャからは、
この関数が何を必要としているのかが分からない。
本当は少なくとも次が必要だ。
userIDcurrencydryRun
それなのに、呼び出し契約がすべて context.Value() の中へ隠れているため、
レビューアーは次の不安を抱えることになる。
- どの呼び出し元が必要値を入れる責任を持つのか
- テスト時に何をセットすればよいのか
- key の重複やtypoをどう防ぐのか
- 値欠落時に400系で返すのか、500系で落ちるのか
これは単なる書き方の好みではない。
依存関係がシグネチャから消えることで、保守時の誤用率が上がるのが本質だ。
設計崩壊はレイヤー越境で加速する
WithValue の乱用は、特にハンドラ層からサービス層へ責務が漏れたときに危険になる。
この構造になると、ctx が
制御信号の器なのか、業務DTOの代用品なのか分からなくなる。
レビューで見たい3つの判断線
1. 業務入力が context.Value() に入っていたら、まず止める
request_id や trace_id のような制御系メタデータならまだ分かる。
しかし、価格、通貨、ユーザーID、権限種別、dry-run指定のような業務入力は別だ。
@Reviewer: `currency` や `dryRun` は制御メタデータではなく業務入力です。contextに埋め込むと関数契約が隠れるため、引数または入力DTOとして明示してください。レビューアーはここで「便利そうだから」で流さないほうがよい。
2. string key と型アサーション依存なら止める
ctx.Value("userID").(string) のようなコードは、
欠落時にpanicしうる上、IDE補助やリファクタ耐性も低い。
@Reviewer: string key と直接型アサーションに依存しており、値欠落時に安全に失敗できません。少なくとも専用key型とgetterを用意し、それでも業務入力なら引数へ戻すべきです。ここは「Goらしい軽量さ」ではなく、
契約の不透明化として見るべきだ。
3. レイヤー越しに ctx.Value() が増殖していたら止める
サービス層、リポジトリ層、ユーティリティ層のあちこちで ctx.Value() が呼ばれ始めると、
どこが入力責務を持っているのか読めなくなる。
@Reviewer: 複数レイヤーで `context.Value()` を業務入力取得に使っており、依存の起点が不明です。値解決は境界層で完結させ、下位層へは明示引数で渡してください。改善方針:contextは制御情報、業務入力は明示引数へ戻す
改善後は、少なくとも業務入力をシグネチャで読める形にしたい。
type CreateOrderInput struct {
UserID string
Currency string
DryRun bool
}
func CreateOrderHandler(w http.ResponseWriter, r *http.Request) {
input := CreateOrderInput{
UserID: r.Header.Get("X-User-ID"),
Currency: r.URL.Query().Get("currency"),
DryRun: r.URL.Query().Get("dryRun") == "true",
}
if err := createOrder(r.Context(), input); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func createOrder(ctx context.Context, input CreateOrderInput) error {
if input.DryRun {
return nil
}
return saveOrder(ctx, input.UserID, input.Currency)
}この形なら、レビュー時にすぐ次が分かる。
- 業務入力が何か
- 入力検証をどこでやるべきか
- テスト時に何を渡せばよいか
ctxの役割が制御信号に限定されているか
WithValue が許容される場面
WithValue 自体が常に悪いわけではない。
次のような値は比較的文脈に合っている。
- request ID
- trace/span 情報
- 認可済み principal のような横断的メタ情報
それでも、乱用防止の観点では次を確認したい。
- 専用key型を使っているか
- getter/setterが限定されているか
- 制御メタデータに用途が限られているか
- 業務判断の主入力になっていないか
@Reviewer: request ID の伝播用途で `WithValue` を使う方針は理解できます。ただし業務分岐条件まで同じ仕組みに載せると責務が崩れるため、用途境界を維持してください。レビュー観点チェックリスト
- 業務入力が
context.Value()に隠れていないか - string key と直接型アサーションに依存していないか
ctxの役割が制御情報に限定されているか- 複数レイヤーで
context.Value()が増殖していないか - 明示引数や入力DTOへ戻したほうが契約が読みやすくならないか
- 値欠落時の失敗形がpanicではなく制御可能になっているか
おわりに
context.WithValue の乱用レビューで見るべきなのは、
値を渡せるかどうかではない。
依存関係がコード上に見えているかである。
引数を減らした結果、契約が見えなくなるなら本末転倒だ。
レビューでは、ctx を何でも運ぶ袋にしないこと、
そして制御情報と業務入力の境界を守ることを優先して確認したい。
