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

[インデックス 14886] ファイルの概要

このコミットは、Go言語の型チェッカーである go/types パッケージにおいて、完全にインポートされたパッケージを明示的にマークする機能を追加するものです。これにより、パッケージの再インポートを不要にし、型チェックの効率を向上させることを目的としています。

コミット

  • コミットハッシュ: 6c3736a527f6c71de0c6503ce3aa948fef592393
  • 作者: Robert Griesemer gri@golang.org
  • 日付: Mon Jan 14 11:01:27 2013 -0800

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6c3736a527f6c71de0c6503ce3aa948fef592393

元コミット内容

go/types: mark completely imported packages as such

R=adonovan
CC=golang-dev
https://golang.org/cl/7103055

変更の背景

Go言語のコンパイラやツールチェーンにおいて、パッケージのインポートは非常に重要な処理です。go/types パッケージは、Goのソースコードを解析し、型情報を構築する役割を担っています。これには、依存する他のパッケージの型情報をインポートする処理が含まれます。

このコミット以前の go/types の実装では、一度インポートしたパッケージであっても、そのパッケージが「完全に」インポートされたかどうかを判断する明確なメカニズムがありませんでした。そのため、同じパッケージが複数回インポートされる可能性があり、その都度、パッケージの解析と型情報の構築が繰り返されることで、パフォーマンスのオーバーヘッドが発生していました。

特に、GcImport 関数(Goコンパイラ形式のバイナリからパッケージをインポートする関数)は、部分的にインポートされたパッケージと完全にインポートされたパッケージを区別できませんでした。このため、たとえ既に完全にインポートされているパッケージであっても、再度インポート処理が実行される無駄が生じていました。

この変更の背景には、このような冗長なインポート処理を排除し、go/types パッケージの効率性とパフォーマンスを向上させるという明確な目的があります。具体的には、パッケージが完全にインポートされたことを示すフラグを導入することで、不必要な再インポートを回避し、型チェック全体の速度を改善しようとしています。

前提知識の解説

Go言語のパッケージとインポート

Go言語では、コードは「パッケージ」という単位で整理されます。パッケージは、関連する機能や型、関数などをまとめたもので、他のパッケージから利用することができます。他のパッケージの機能を利用するには、import キーワードを使ってそのパッケージをインポートする必要があります。

import (
    "fmt"
    "net/http"
)

func main() {
    fmt.Println("Hello, Go!")
    // httpパッケージの機能を利用
}

Goのビルドシステムは、ソースコードをコンパイルする際に、依存するパッケージを解決し、それらの型情報を利用してコードの正当性を検証します。

go/types パッケージ

go/types は、Go言語の標準ライブラリの一部であり、Goの型システムをプログラム的に扱うためのパッケージです。これは主に、Goコンパイラ、リンター、IDEなどのツールが、Goのソースコードを解析し、型チェックを行うために使用されます。

go/types の主な機能は以下の通りです。

  • AST (Abstract Syntax Tree) の解析: Goのソースコードを抽象構文木に変換します。
  • 型情報の構築: ASTから、変数、関数、型などの型情報を抽出・構築します。
  • 型チェック: Goの型規則に従って、コードが正しく型付けされているかを検証します。これには、型の一致、インターフェースの実装、メソッドの解決などが含まれます。
  • スコープの管理: 変数や型の可視性(スコープ)を管理します。
  • パッケージのインポート: 依存する他のGoパッケージの型情報を読み込み、現在のパッケージの型チェックに利用します。

Goコンパイラの内部構造と型チェック

Goコンパイラは、複数のフェーズを経てソースコードを実行可能なバイナリに変換します。大まかには以下のフェーズがあります。

  1. 字句解析 (Lexing): ソースコードをトークンに分解します。
  2. 構文解析 (Parsing): トークンからASTを構築します。
  3. 型チェック (Type Checking): ASTを走査し、go/types パッケージなどを用いて型情報を解決し、型エラーを検出します。このフェーズで、インポートされたパッケージの型情報が利用されます。
  4. 中間コード生成 (Intermediate Code Generation): 型チェックが完了したASTから、中間表現(IR)を生成します。
  5. 最適化 (Optimization): 中間コードを最適化します。
  6. コード生成 (Code Generation): 最終的な機械語コードを生成します。

このコミットは、特に「型チェック」フェーズにおけるパッケージインポートの効率化に焦点を当てています。

gcimporter とエクスポートデータ

Goコンパイラは、コンパイル済みのパッケージの型情報を「エクスポートデータ」として保存します。このエクスポートデータは、Goコンパイラ独自のバイナリ形式で、他のパッケージがそのパッケージをインポートする際に利用されます。gcimporter は、このGoコンパイラ形式のエクスポートデータを読み込み、go/types パッケージが利用できる形式に変換する役割を担っています。

GcImport 関数は、この gcimporter を介して、指定されたパスのパッケージをインポートし、その型情報を go/types の内部データ構造にロードします。

技術的詳細

このコミットの核心は、Package 構造体に Complete という新しいフィールドを追加し、パッケージが完全にインポートされたかどうかを追跡することです。

Package.Complete フィールドの導入

src/pkg/go/types/objects.go にある Package 構造体に、Complete bool というフィールドが追加されました。

type Package struct {
    Name     string
    Path     string              // import path, "" for current (non-imported) package
    Scope    *Scope              // package-level scope
    Imports  map[string]*Package // map of import paths to imported packages
    Complete bool                // if set, this package was imported completely
    spec *ast.ImportSpec
}

この Complete フィールドは、そのパッケージがエクスポートデータから完全に読み込まれ、型情報が完全に構築された場合に true に設定されます。

GcImport 関数における最適化

src/pkg/go/types/gcimporter.goGcImport 関数が変更され、パッケージの再インポートを避けるロジックが追加されました。 変更前は、imports[id] に部分的にインポートされたパッケージが含まれている可能性があり、常に完全なインポート処理を続行する必要がありました。しかし、Complete フィールドの導入により、GcImport はまず imports マップをチェックし、もし imports[id] に既に存在するパッケージがあり、かつその Complete フィールドが true であれば、そのパッケージをそのまま返すようになりました。これにより、不必要なファイルI/Oや解析処理がスキップされます。

// no need to re-import if the package was imported completely before
if pkg = imports[id]; pkg != nil && pkg.Complete {
    return
}

GcImportDataparseExport における Complete の設定

src/pkg/go/types/gcimporter.goparseExport 関数(エクスポートデータを実際に解析する関数)の最後に、インポート処理がエラーなく完了した場合に pkg.Complete = true を設定する行が追加されました。これにより、パッケージが完全にインポートされたことが明示的にマークされます。

// package was imported completely and without errors
pkg.Complete = true

check.go における GcImport の直接利用

src/pkg/go/types/check.go では、以前は GcImport をラップして、既にインポートされたパッケージを追跡するための独自の imported マップを持っていました。これは、GcImport 自体がパッケージの完全なインポート状態を追跡できなかったための一時的な回避策でした。 このコミットにより、GcImportComplete フラグを適切に処理するようになったため、check.go 内のこの冗長なラッパーは削除され、imp = GcImport と直接 GcImport を使用するようになりました。これにより、コードが簡素化され、go/types パッケージ全体のインポートロジックが一元化されました。

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

このコミットで変更されたファイルは以下の3つです。

  1. src/pkg/go/types/check.go:

    • check 関数内の imp 変数の初期化ロジックが変更されました。
    • 以前存在した、GcImport をラップしてインポート済みパッケージを追跡する匿名関数が削除され、直接 imp = GcImport となりました。
    • これにより、14行が削除されました。
  2. src/pkg/go/types/gcimporter.go:

    • GcImport 関数に、Package.Complete フィールドをチェックして再インポートを避けるロジックが追加されました。
    • parseExport 関数の最後に、インポートが完了したパッケージの Complete フィールドを true に設定する行が追加されました。
    • GcImportData 関数のコメントが更新され、Complete フィールドの役割が説明されました。
    • 全体で14行が追加され、10行が削除されました。
  3. src/pkg/go/types/objects.go:

    • Package 構造体に Complete bool フィールドが追加されました。
    • これにより、1行が追加され、4行が削除されました(おそらくインデントの変更によるもの)。

コアとなるコードの解説

src/pkg/go/types/check.go の変更

--- a/src/pkg/go/types/check.go
+++ b/src/pkg/go/types/check.go
@@ -418,19 +418,7 @@ func check(ctxt *Context, fset *token.FileSet, files []*ast.File) (pkg *Package,\n 	// resolve identifiers\n 	imp := ctxt.Import\n 	if imp == nil {\n-\t\t// wrap GcImport to import packages only once by default.\n-\t\t// TODO(gri) move this into resolve\n-\t\timported := make(map[string]bool)\n-\t\timp = func(imports map[string]*Package, path string) (*Package, error) {\n-\t\t\tif imported[path] && imports[path] != nil {\n-\t\t\t\treturn imports[path], nil\n-\t\t\t}\n-\t\t\tpkg, err := GcImport(imports, path)\n-\t\t\tif err == nil {\n-\t\t\t\timported[path] = true\n-\t\t\t}\n-\t\t\treturn pkg, err\n-\t\t}\
+\t\timp = GcImport
 	}
 	pkg, methods := check.resolve(imp)
 	check.pkg = pkg

この変更は、go/types の型チェック処理の入り口である check 関数におけるインポートロジックの簡素化を示しています。以前は、GcImport がパッケージの完全なインポート状態を保証しなかったため、check.go 側で imported マップを使って重複インポートを防ぐためのラッパー関数が用意されていました。しかし、GcImport 自体がその責任を負うようになったため、このラッパーは不要となり、直接 GcImport を使用するようになりました。これは、責任の適切な分離とコードのクリーンアップを意味します。

src/pkg/go/types/gcimporter.go の変更

--- a/src/pkg/go/types/gcimporter.go
+++ b/src/pkg/go/types/gcimporter.go
@@ -77,10 +77,13 @@ func FindPkg(path, srcDir string) (filename, id string) {
 // adds the corresponding package object to the imports map indexed by id,
 // and returns the object.
 //
-// The imports map must contains all packages already imported, and no map
-// entry with id as the key must be present. The data reader position must
-// be the beginning of the export data section. The filename is only used
-// in error messages.
+// The imports map must contains all packages already imported. The data
+// reader position must be the beginning of the export data section. The
+// filename is only used in error messages.
+//
+// If imports[id] contains the completely imported package, that package
+// can be used directly, and there is no need to call this function (but
+// there is also no harm but for extra time used).
 //
 func GcImportData(imports map[string]*Package, filename, id string, data *bufio.Reader) (pkg *Package, err error) {
  	// support for gcParser error handling
@@ -118,12 +121,10 @@ func GcImport(imports map[string]*Package, path string) (pkg *Package, err error\
  		return
  	}
  
-	// Note: imports[id] may already contain a partially imported package.
-	//       We must continue doing the full import here since we don\'t
-	//       know if something is missing.
-	// TODO: There\'s no need to re-import a package if we know that we
-	//       have done a full import before. At the moment we cannot
-	//       tell from the available information in this function alone.
+	// no need to re-import if the package was imported completely before
+	if pkg = imports[id]; pkg != nil && pkg.Complete {
+		return
+	}
  
  	// open file
  	f, err := os.Open(filename)
@@ -900,5 +901,8 @@ func (p *gcParser) parseExport() *Package {
  		p.errorf("expected no scanner errors, got %d", n)
  	}\n \n+\t// package was imported completely and without errors
+\tpkg.Complete = true
+\n 	return pkg
 }\n```
このファイルへの変更は、このコミットの主要なロジックを含んでいます。
*   `GcImportData` のコメント更新は、`Complete` フィールドの導入によって、`GcImport` の呼び出し元が事前にパッケージが完全にインポートされているかを確認できるようになったことを示唆しています。
*   `GcImport` 関数内の `if pkg = imports[id]; pkg != nil && pkg.Complete` ブロックは、パフォーマンス最適化の核心です。これにより、既に完全にインポートされたパッケージは、再度解析されることなく即座に返されます。これは、大規模なプロジェクトや多くの依存関係を持つプロジェクトにおいて、型チェックの時間を大幅に短縮する可能性があります。
*   `parseExport` 関数での `pkg.Complete = true` の設定は、エクスポートデータの解析が成功し、パッケージの型情報が完全にメモリにロードされたことを明示的にマークします。これにより、`GcImport` が将来の呼び出しでこのパッケージをスキップできるようになります。

### `src/pkg/go/types/objects.go` の変更

```diff
--- a/src/pkg/go/types/objects.go
+++ b/src/pkg/go/types/objects.go
@@ -23,10 +23,11 @@ type Object interface {
 
 // A Package represents the contents (objects) of a Go package.
 type Package struct {
-\tName    string
-\tPath    string              // import path, "" for current (non-imported) package
-\tScope   *Scope              // package-level scope
-\tImports map[string]*Package // map of import paths to imported packages
+\tName     string
+\tPath     string              // import path, "" for current (non-imported) package
+\tScope    *Scope              // package-level scope
+\tImports  map[string]*Package // map of import paths to imported packages
+\tComplete bool                // if set, this package was imported completely
 
 \tspec *ast.ImportSpec
 }

この変更は、Package 構造体への Complete フィールドの追加です。これは、パッケージが完全にインポートされたかどうかを示す状態を保持するための、シンプルかつ効果的な方法です。このフィールドの追加により、go/types パッケージ全体でインポート状態を一貫して管理できるようになります。

関連リンク

参考にした情報源リンク