#78889active
cmd/vet, x/tools/go/analysis/passes/errorsastype: detect misuse of \\`AsType\\` in \\`else if\\`
新規提案
要約
AIによる要約であり、誤りを含む場合があります。
概要
errors.AsType(Go 1.26で導入された型安全なエラー型チェック関数)をelse ifチェーンで連続使用した際に発生するバグパターンを検出するための静的解析器errorsastypeを、x/tools/go/analysis/passes/errorsastypeとして公開し、go vetの検査スイートにも追加するというproposalです。
ステータス変更
(新規) → active
2026年5月13日のProposal Review Meetingでactiveコラムへ追加されました。@aclementsがレビューの中で、現在の提案で検出するパターン(errors.AsTypeのelse if連鎖)より一般化した形でのエラー変数使用も問題になりうると指摘し、引き続き議論が継続することになりました。
技術的背景
現状の問題点
Go 1.26で追加されたerrors.AsType[T]は、型パラメータを用いてエラー型チェックをより簡潔・安全に書けるようにした関数です。しかし、複数のエラー型をelse ifでチェーンして試みると、以下のような罠があります。
var err error
if err, ok := errors.AsType[*FooErr](err); ok {
doSth(err)
} else if err, ok := errors.AsType[*BarErr](err); ok {
// 問題: ここでの err は outer の error 変数ではなく、
// 上の if 文のブロックスコープで宣言された err (型: *FooErr) が
// スコープに残っており、AsType の第1引数がそれを参照してしまう
doSth(err)
}
else ifブロックでは、直前のifの初期化ステートメントで宣言されたerr(*FooErr型のゼロ値、すなわち型付きnil)がerrors.AsTypeの引数として渡されてしまいます。これは以下の2つの問題を引き起こします。
- 到達不能な条件:
*FooErr型のゼロ値((*FooErr)(nil))を渡しているため、*BarErrへのキャストは実質的に常に失敗する可能性が高くなります。 - ランタイムパニック: もし
*FooErr型にUnwrapなどのフィールドにアクセするメソッドが定義されている場合、型付きnilレシーバーでの呼び出しはパニックを引き起こします。
この問題は既存の「変数シャドウイング」検出器(issue #75368など)では捕捉できません。なぜなら、シャドウイング検出器は外側の変数が内側でシャドウされていることは分かりますが、else ifの初期化ステートメント間の依存関係までは追跡しないためです。
提案された解決策
errorsastypeという新しいアナライザーを作成し、以下の形で提供します。
x/tools/go/analysis/passes/errorsastypeとして公開(goplsの内部実装から昇格)go vetの検査スイートに追加(Go 1.27を目標)
アナライザーの検出ロジックは以下の通りです。
*ast.IfStmtチェーンを走査する- 初期化ステートメントが
errors.AsTypeの:=短変数宣言であるif/else ifを検出する - 各アームの第1結果変数(型付き戻り値)を記録する
- 次の
else ifでerrors.AsTypeの第1引数が、直前のアームの結果オブジェクトを参照しているかを型情報で確認する
これによって何ができるようになるか
このアナライザーにより、開発者は以下のようなシナリオで誤ったコードをコンパイル時またはgo vet実行時に検出できるようになります。
コード例
// Before(問題のあるコード): else if の AsType に誤った引数が渡される
var err error = getError()
if err, ok := errors.AsType[*FooErr](err); ok {
handleFoo(err)
} else if err, ok := errors.AsType[*BarErr](err); ok {
// err は *FooErr 型のゼロ値(型付きnil)が渡されている
// この分岐は期待通りに動作しない可能性があり、最悪パニックする
handleBar(err)
}
// After(正しい書き方): 外側の err を別名で保持するか、独立した if を使う
origErr := getError()
if fooErr, ok := errors.AsType[*FooErr](origErr); ok {
handleFoo(fooErr)
} else if barErr, ok := errors.AsType[*BarErr](origErr); ok {
handleBar(barErr)
}
実践的なユースケースとしては、以下のような場面でこのアナライザーが有効です。
- 複数のカスタムエラー型を順次チェックするエラーハンドリングルーティン
- HTTPハンドラーやミドルウェアにおける多段エラー分類処理
- エラーラッピングライブラリを使用したコードのマイグレーション(
errors.Asからerrors.AsTypeへの書き換え後に誤ったパターンが混入した場合)
議論のハイライト
- 提案の経緯: コードレビューで
errors.AsTypeのelse if連鎖が自然な書き方に見えながら実は誤りであることが発見されたことをきっかけに、mateusz834氏がissueを作成し、実装CL(CL 770360)を即座に提出するほど問題意識が高かった。 - 実装アプローチの議論:
@abhay1999がerrorsasアナライザーへの組み込みを提案する一方、@mateusz834はすでにドラフト実装を持っており、@adonovanが最終的に独立したerrorsastypeアナライザーとして分離することを推奨した。これにより既存のerrorsasアナライザーへの影響を避けられ、goplsへの即時展開も容易になる。 @adonovanの代替案:AsType[T](e)においてTとeの型がともに非インターフェース型である場合を検出する、よりシンプルなアプローチを提案したが、インターフェース型の場合やUnwrap/Asメソッドを持つ型の場合にも同様の問題が起きうることを@mateusz834が指摘し、現在の「前アームの結果変数が次アームの引数になっているか」という検出が採用された。go vetへの追加にはproposalが必要:@mateusz834が既存のerrorsasアナライザーはすでにgo vetに含まれていることに気づき、その能力拡張にはproposalプロセスが必要であることを確認。issueをproposalとして再整理した。@aclementsのフィードバック(2026-05-13レビュー): 提案の検出範囲をAsType限定から広げ、elseブランチで前のアームの変数が使われているケース全般(AsType以外も含む)を検出すべきとの意見が出ており、仕様の詳細についてさらなる議論が必要とされている。