[インデックス 15452] ファイルの概要
このコミットは、Go言語の型チェッカーである go/types
パッケージと、それを利用するコマンドラインツール gotype
の堅牢性とユーザビリティを向上させるための変更を含んでいます。主な目的は、複数のエラーが存在する場合でも型チェックが途中で停止せず、より多くのエラーを報告できるようにすること、そして内部的なパニックに対するハンドリングを改善することです。
コミット
commit 60066754fd6d080e6f0b08d88369beea4b54b801
Author: Robert Griesemer <gri@golang.org>
Date: Tue Feb 26 14:33:24 2013 -0800
go/types: be more robust in presence of multiple errors
- better documentation of Check
- better handling of (explicit) internal panics
- gotype: don't stop after 1st error
R=adonovan, r
CC=golang-dev
https://golang.org/cl/7406052
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/60066754fd6d080e6f0b08d88369beea4b54b801
元コミット内容
このコミットは、以下の3つの主要な改善を目的としています。
Check
関数のドキュメント改善:go/types
パッケージのContext.Check
メソッドのドキュメントをより明確にし、エラーハンドラが設定されている場合の動作(複数のエラーを報告し、型チェックを継続するが、結果が不完全になる可能性があること)を詳細に記述しています。- 内部パニックのより良いハンドリング:
go/types
パッケージ内部で発生する可能性のある予期せぬパニック(unreachable()
など)に対して、より堅牢な処理を導入しています。これにより、ライブラリとして利用された際に、内部エラーが呼び出し元アプリケーションのクラッシュに直結するのを防ぎます。 gotype
コマンドが最初のエラーで停止しないようにする:gotype
コマンドラインツールが、型チェック中に最初に見つかったエラーで処理を中断するのではなく、複数のエラーを収集して報告するように変更されました。これにより、開発者は一度の実行でより多くの問題点を把握できるようになります。
変更の背景
Go言語の型チェックは、コンパイラだけでなく、IDE、リンター、静的解析ツールなど、様々な開発ツールの中核をなす機能です。これらのツールが単一のエラーで処理を停止してしまうと、開発者はエラーを一つずつ修正し、その都度ツールを再実行する必要があり、開発効率が著しく低下します。
このコミットの背景には、以下のような課題認識があったと考えられます。
- 開発者の生産性向上: 複数の型エラーが同時に存在する場合、それらを一度に報告することで、開発者はより効率的にコードを修正できるようになります。
- ツールの堅牢性:
go/types
パッケージが内部的なエラー(特にpanic
)によって予期せず終了してしまうと、それを利用するツール全体がクラッシュする可能性があります。これを防ぎ、より安定した動作を保証することが求められていました。 - APIの明確化:
go/types
パッケージのCheck
メソッドの挙動、特にエラーハンドリング時の動作が不明瞭であったため、そのドキュメントを改善し、利用者が正しく理解できるようにする必要がありました。
これらの改善は、Go言語のエコシステム全体における開発体験の向上に寄与するものです。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の概念とパッケージに関する知識が必要です。
- Go言語の型システム: Goは静的型付け言語であり、コンパイル時に厳密な型チェックが行われます。
go/types
パッケージはこの型チェックのロジックを実装しています。 - 抽象構文木 (AST): Goのソースコードは、
go/parser
パッケージによって解析され、go/ast
パッケージで定義される抽象構文木 (AST) として表現されます。型チェッカーはこのASTを入力として処理します。 go/types
パッケージ: Go言語のソースコードの型情報を解決し、型エラーを検出するための標準ライブラリです。コンパイラ (go/compiler
) の中核部分であり、gofmt
やgo vet
といったツールでも利用されます。このパッケージは、ASTを走査し、識別子の解決、型の推論、型の一貫性チェックなどを行います。go/token
パッケージ: ソースコード内の位置情報(ファイル名、行番号、列番号)を扱うためのパッケージです。エラーメッセージの正確な位置を示すために使用されます。go/scanner
パッケージ: ソースコードを字句解析(トークン化)するためのパッケージです。scanner.ErrorList
は、複数の字句解析エラーをまとめて保持する型です。panic
とrecover
: Go言語におけるエラーハンドリングのメカニズムの一つです。panic
はプログラムの異常終了を引き起こしますが、defer
ステートメント内でrecover
関数を呼び出すことで、パニックを捕捉し、プログラムのクラッシュを防ぎ、回復処理を行うことができます。このコミットでは、go/types
パッケージの内部的な予期せぬパニックを捕捉し、より制御された方法でエラーを報告するために利用されています。gotype
コマンド:go/types
パッケージを利用してGoソースファイルの型チェックを行うためのコマンドラインツールです。このコミット以前は、最初のエラーで停止する挙動でした。
技術的詳細
このコミットは、主に以下の技術的な変更を含んでいます。
-
gotype
ツールにおける複数エラー報告の実現 (src/pkg/exp/gotype/gotype.go
):- 従来の
exitCode
変数に代わり、errorCount
変数を導入し、検出されたエラーの総数を追跡するようにしました。 report
関数がscanner.ErrorList
型のエラーを適切に処理し、リスト内の全てのエラーをerrorCount
に加算するように変更されました。processPackage
関数内でtypes.Context
を初期化する際に、カスタムのエラーハンドラ (Error: func(err error)
) を設定しています。このハンドラは、go/types
パッケージがエラーを検出するたびに呼び出されます。- カスタムエラーハンドラは、
*allErrors
フラグがfalse
の場合(デフォルト)、エラー数が10個に達するとpanic(bailout{})
を発行して型チェックを意図的に中断します。これにより、無限にエラーを報告し続けることを防ぎつつ、ある程度の数のエラーを一度に確認できるようにしています。 processPackage
関数にはdefer
とrecover
を用いたパニックハンドリングが追加されました。これにより、bailout{}
による意図的なパニックは捕捉して無視し、それ以外の予期せぬパニックは再パニックさせることで、デバッグを容易にしつつ、gotype
ツールがクラッシュするのを防いでいます。
- 従来の
-
types.Check
メソッドのドキュメント改善 (src/pkg/go/types/api.go
):Context.Check
メソッドのコメントが大幅に加筆・修正されました。- 特に、
Context
にError
ハンドラが設定されている場合、Check
メソッドは最初のエラーで停止せず、パッケージ全体をチェックし続けることが明記されました。 - また、エラーが発生した場合、返される
*Package
オブジェクトが部分的にしか型チェックされておらず、不完全な状態(オブジェクトやインポートが欠落しているなど)である可能性があることも明確に記述されました。これは、エラーが発生した後の型情報の信頼性に関する重要な注意点です。
-
型チェッカーの内部堅牢性向上 (
src/pkg/go/types/check.go
):checker.object
関数内で、ast.ValueSpec
(通常の変数宣言)とast.AssignStmt
(短い変数宣言:=
)の処理がswitch
ステートメントで明示的に分岐されました。これにより、短い変数宣言の右辺が型チェックに失敗した場合でも、左辺の識別子が型付けされない状態を適切に処理できるようになりました。check
関数のトップレベルのrecover
ブロックが変更されました。以前は予期せぬパニックが発生した場合に常に再パニックしていましたが、debug
フラグ(このコミットではtrue
に固定)が導入され、デバッグ時以外は内部パニックを捕捉し、一般的なエラーとして報告する(ただし、このコミットではまだ再パニックする挙動)ように意図されています。これにより、go/types
をライブラリとして利用するアプリケーションが、内部的なバグによってクラッシュするのを防ぐための基盤が作られました。
-
エラーメッセージのフォーマット改善 (
src/pkg/go/types/errors.go
):checker.formatMsg
関数内で、token.Pos
型の位置情報を文字列に変換する際に、check.fset.Position(a).String()
を使用するように変更されました。これにより、エラーメッセージに表示されるファイル位置のフォーマットがより一貫性のあるものになります。
これらの変更は、go/types
パッケージがより多くのエラーを報告し、内部的な問題に対してより堅牢になることで、Go言語のツールチェイン全体の信頼性と使いやすさを向上させています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。
-
src/pkg/exp/gotype/gotype.go
:var errorCount int
の追加とvar exitCode = 0
の削除。report(err error)
関数のロジック変更。scanner.ErrorList
の処理とerrorCount
のインクリメント。processPackage(fset *token.FileSet, files []*ast.File)
関数内でのtypes.Context
の初期化とカスタムエラーハンドラの設定。processPackage
関数内でのdefer
/recover
ブロックの追加。main()
関数でのos.Exit(errorCount > 0 ? 2 : 0)
の変更。
-
src/pkg/go/types/api.go
:type Context struct { ... }
のCheck
メソッドのドキュメントコメントの変更。
-
src/pkg/go/types/check.go
:func (check *checker) object(obj Object, cycleOk bool)
関数内のobj.decl
のswitch
ステートメントの追加。func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (*Package, error)
関数内のrecover
ブロックの変更(debug
フラグの導入)。
コアとなるコードの解説
src/pkg/exp/gotype/gotype.go
の変更
gotype
ツールは、go/types
パッケージの Check
関数を呼び出して型チェックを実行します。このコミットの主要な変更は、Check
関数に渡す types.Context
にカスタムのエラーハンドラを設定することで、gotype
が最初のエラーで停止しないようにした点です。
// processPackage は与えられたファイルセットとASTファイルリストに対して型チェックを実行します。
func processPackage(fset *token.FileSet, files []*ast.File) {
type bailout struct{} // 意図的な中断を示すためのカスタムパニック型
ctxt := types.Context{
Error: func(err error) { // go/types がエラーを検出した際に呼び出されるカスタムハンドラ
if !*allErrors && errorCount >= 10 { // -allErrors フラグがなければ、10個のエラーで中断
panic(bailout{}) // 意図的にパニックを発行
}
report(err) // エラーを報告し、errorCount をインクリメント
},
}
defer func() { // パニックを捕捉するための defer
switch err := recover().(type) {
case nil, bailout: // パニックがなければ何もしない、または bailout パニックなら正常終了
// 何もしない
default:
panic(err) // その他の予期せぬパニックは再パニックさせる
}
}()
ctxt.Check(fset, files) // 型チェックを実行
}
このコードスニペットでは、types.Context
の Error
フィールドに匿名関数を割り当てています。go/types
パッケージは型エラーを検出するたびにこの関数を呼び出します。このハンドラ内で errorCount
をインクリメントし、*allErrors
フラグが false
の場合は、エラー数が10個に達すると bailout{}
型のパニックを発生させます。このパニックは defer
ブロックで捕捉され、gotype
ツールがクラッシュすることなく、これまでに収集したエラーを報告して終了できるようにします。これにより、gotype
は単一のエラーで停止せず、複数のエラーを一度に表示できるようになりました。
src/pkg/go/types/api.go
の Context.Check
ドキュメント変更
Check
メソッドのドキュメントは、go/types
パッケージの利用者がその挙動を正しく理解するために非常に重要です。
// Check resolves and typechecks a set of package files within the given
// context. It returns the package and the first error encountered, if
// any. If the context's Error handler is nil, Check terminates as soon
// as the first error is encountered; otherwise it continues until the
// entire package is checked. If there are errors, the package may be
// only partially type-checked, and the resulting package may be incomplete
// (missing objects, imports, etc.).
func (ctxt *Context) Check(fset *token.FileSet, files []*ast.File) (*Package, error) {
return check(ctxt, fset, files)
}
変更後のドキュメントは、Context
に Error
ハンドラが設定されている場合、Check
が最初のエラーで停止せず、パッケージ全体をチェックし続けることを明確に述べています。また、エラーが発生した場合、返される *Package
オブジェクトが不完全である可能性があるという重要な注意点も追加されています。これは、go/types
を利用して型情報を取得するツール開発者にとって、エラー発生時のデータ信頼性を理解する上で不可欠な情報です。
src/pkg/go/types/check.go
の object
関数と check
関数の変更
checker.object
関数は、ASTノードからオブジェクト(変数、関数、型など)を解決する際に呼び出されます。この変更は、特に短い変数宣言 (:=
) のようなケースで、右辺の式が型チェックに失敗した場合の堅牢性を高めています。
func (check *checker) object(obj Object, cycleOk bool) {
// ... (既存のコード) ...
case *Var:
if obj.Type != nil {
return
}
if obj.decl == nil {
unreachable() // オブジェクトは常に宣言を持つべき
}
switch d := obj.decl.(type) { // 宣言の型によって分岐
case *ast.Field: // 関数パラメータ
unreachable() // 関数パラメータは収集時に常に型付けされている
case *ast.ValueSpec: // 適切な変数宣言 (var x int = ...)
obj.visited = true
check.valueSpec(d.Pos(), obj, d.Names, d, 0)
case *ast.AssignStmt: // 短い変数宣言 (x := ...)
// ここに到達した場合、短い変数宣言の右辺が型チェックに失敗し、
// そのため左辺に型がない状態である。
obj.visited = true
obj.Type = Typ[Invalid] // 型を Invalid に設定
default:
unreachable() // その他のケースは予期しない
}
// ... (既存のコード) ...
}
この変更により、ast.AssignStmt
(短い変数宣言)の右辺が型チェックに失敗した場合、その左辺の変数 (obj
) の型が Typ[Invalid]
に設定されるようになりました。これにより、型チェッカーは不完全な情報でも処理を継続し、後続のチェックでこの無効な型を適切に扱うことができます。
また、check
関数の recover
ブロックの変更は、go/types
パッケージの内部的なパニックに対するより制御されたハンドリングを導入しています。
func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (pkg *Package, err error) {
// ... (既存のコード) ...
defer func() {
if p := recover(); p != nil {
// ... (既存のコード) ...
default:
// 予期せぬパニック: クライアントをクラッシュさせない
const debug = true
if debug {
check.dump("INTERNAL PANIC: %v", p)
panic(p) // デバッグのために再パニック
}
// TODO(gri) このシナリオのテストケースを追加
err = fmt.Errorf("types internal error: %v", p)
}
}
}()
// ... (既存のコード) ...
}
この変更は、go/types
パッケージがライブラリとして利用される際に、内部的な panic
が呼び出し元アプリケーションをクラッシュさせるのを防ぐためのものです。debug
フラグが false
の場合(このコミットでは true
に固定されているため、まだ再パニックする)、予期せぬパニックは捕捉され、より一般的なエラーメッセージとして報告されるようになります。これにより、go/types
パッケージの堅牢性が向上し、それを基盤とするツールがより安定して動作するようになります。
関連リンク
- Go言語公式ウェブサイト: https://go.dev/
go/types
パッケージのドキュメント (Go 1.0.3 のもの): https://pkg.go.dev/go/types@go1.0.3 (コミット当時のバージョンに近いドキュメント)go/ast
パッケージのドキュメント: https://pkg.go.dev/go/astgo/token
パッケージのドキュメント: https://pkg.go.dev/go/tokengo/scanner
パッケージのドキュメント: https://pkg.go.dev/go/scanner- Gerrit Change 7406052: https://golang.org/cl/7406052 (このコミットの元のレビューページ)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
go/types
およびexp/gotype
ディレクトリ) - Go言語の
panic
とrecover
に関する一般的な解説記事 - Go言語のASTに関する解説記事