Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つの主要な改善を目的としています。

  1. Check 関数のドキュメント改善: go/types パッケージの Context.Check メソッドのドキュメントをより明確にし、エラーハンドラが設定されている場合の動作(複数のエラーを報告し、型チェックを継続するが、結果が不完全になる可能性があること)を詳細に記述しています。
  2. 内部パニックのより良いハンドリング: go/types パッケージ内部で発生する可能性のある予期せぬパニック(unreachable() など)に対して、より堅牢な処理を導入しています。これにより、ライブラリとして利用された際に、内部エラーが呼び出し元アプリケーションのクラッシュに直結するのを防ぎます。
  3. 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) の中核部分であり、gofmtgo vet といったツールでも利用されます。このパッケージは、ASTを走査し、識別子の解決、型の推論、型の一貫性チェックなどを行います。
  • go/token パッケージ: ソースコード内の位置情報(ファイル名、行番号、列番号)を扱うためのパッケージです。エラーメッセージの正確な位置を示すために使用されます。
  • go/scanner パッケージ: ソースコードを字句解析(トークン化)するためのパッケージです。scanner.ErrorList は、複数の字句解析エラーをまとめて保持する型です。
  • panicrecover: Go言語におけるエラーハンドリングのメカニズムの一つです。panic はプログラムの異常終了を引き起こしますが、defer ステートメント内で recover 関数を呼び出すことで、パニックを捕捉し、プログラムのクラッシュを防ぎ、回復処理を行うことができます。このコミットでは、go/types パッケージの内部的な予期せぬパニックを捕捉し、より制御された方法でエラーを報告するために利用されています。
  • gotype コマンド: go/types パッケージを利用してGoソースファイルの型チェックを行うためのコマンドラインツールです。このコミット以前は、最初のエラーで停止する挙動でした。

技術的詳細

このコミットは、主に以下の技術的な変更を含んでいます。

  1. 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 関数には deferrecover を用いたパニックハンドリングが追加されました。これにより、bailout{} による意図的なパニックは捕捉して無視し、それ以外の予期せぬパニックは再パニックさせることで、デバッグを容易にしつつ、gotype ツールがクラッシュするのを防いでいます。
  2. types.Check メソッドのドキュメント改善 (src/pkg/go/types/api.go):

    • Context.Check メソッドのコメントが大幅に加筆・修正されました。
    • 特に、ContextError ハンドラが設定されている場合、Check メソッドは最初のエラーで停止せず、パッケージ全体をチェックし続けることが明記されました。
    • また、エラーが発生した場合、返される *Package オブジェクトが部分的にしか型チェックされておらず、不完全な状態(オブジェクトやインポートが欠落しているなど)である可能性があることも明確に記述されました。これは、エラーが発生した後の型情報の信頼性に関する重要な注意点です。
  3. 型チェッカーの内部堅牢性向上 (src/pkg/go/types/check.go):

    • checker.object 関数内で、ast.ValueSpec(通常の変数宣言)と ast.AssignStmt(短い変数宣言 :=)の処理が switch ステートメントで明示的に分岐されました。これにより、短い変数宣言の右辺が型チェックに失敗した場合でも、左辺の識別子が型付けされない状態を適切に処理できるようになりました。
    • check 関数のトップレベルの recover ブロックが変更されました。以前は予期せぬパニックが発生した場合に常に再パニックしていましたが、debug フラグ(このコミットでは true に固定)が導入され、デバッグ時以外は内部パニックを捕捉し、一般的なエラーとして報告する(ただし、このコミットではまだ再パニックする挙動)ように意図されています。これにより、go/types をライブラリとして利用するアプリケーションが、内部的なバグによってクラッシュするのを防ぐための基盤が作られました。
  4. エラーメッセージのフォーマット改善 (src/pkg/go/types/errors.go):

    • checker.formatMsg 関数内で、token.Pos 型の位置情報を文字列に変換する際に、check.fset.Position(a).String() を使用するように変更されました。これにより、エラーメッセージに表示されるファイル位置のフォーマットがより一貫性のあるものになります。

これらの変更は、go/types パッケージがより多くのエラーを報告し、内部的な問題に対してより堅牢になることで、Go言語のツールチェイン全体の信頼性と使いやすさを向上させています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更箇所は以下のファイルと関数に集中しています。

  1. 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) の変更。
  2. src/pkg/go/types/api.go:

    • type Context struct { ... }Check メソッドのドキュメントコメントの変更。
  3. src/pkg/go/types/check.go:

    • func (check *checker) object(obj Object, cycleOk bool) 関数内の obj.declswitch ステートメントの追加。
    • 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.ContextError フィールドに匿名関数を割り当てています。go/types パッケージは型エラーを検出するたびにこの関数を呼び出します。このハンドラ内で errorCount をインクリメントし、*allErrors フラグが false の場合は、エラー数が10個に達すると bailout{} 型のパニックを発生させます。このパニックは defer ブロックで捕捉され、gotype ツールがクラッシュすることなく、これまでに収集したエラーを報告して終了できるようにします。これにより、gotype は単一のエラーで停止せず、複数のエラーを一度に表示できるようになりました。

src/pkg/go/types/api.goContext.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)
}

変更後のドキュメントは、ContextError ハンドラが設定されている場合、Check が最初のエラーで停止せず、パッケージ全体をチェックし続けることを明確に述べています。また、エラーが発生した場合、返される *Package オブジェクトが不完全である可能性があるという重要な注意点も追加されています。これは、go/types を利用して型情報を取得するツール開発者にとって、エラー発生時のデータ信頼性を理解する上で不可欠な情報です。

src/pkg/go/types/check.goobject 関数と 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言語の公式ドキュメント
  • Go言語のソースコード (特に go/types および exp/gotype ディレクトリ)
  • Go言語の panicrecover に関する一般的な解説記事
  • Go言語のASTに関する解説記事