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

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

このコミットは、Go言語のツールチェインの一部である cmd/api において、token.FileSet の利用方法を最適化するものです。具体的には、これまで Walker 構造体の各インスタンスがそれぞれ独自の token.FileSet を持っていたのを、グローバルな単一の token.FileSet を使用するように変更しています。これは、将来の変更("future CL")に向けた準備作業とされています。

コミット

Author: Brad Fitzpatrick bradfitz@golang.org Date: Tue Nov 13 09:59:46 2012 -0800

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

https://github.com/golang/go/commit/0eb42fa6e45b09e650a0abca24da7916a3e23384

元コミット内容

cmd/api: use one token.FileSet for all contexts

Prep for future CL.

R=gri
CC=golang-dev
https://golang.org/cl/6843048

変更の背景

このコミットの主な背景は、コミットメッセージに明記されている「将来のCL(Change List)に向けた準備」です。cmd/api ツールは、GoのAPIサーフェスを分析し、その変更を追跡するために使用されます。このツールが複数のGoソースファイルを解析する際、各ファイルは token.FileSet に登録され、ファイル内の位置情報(行番号、列番号など)が管理されます。

以前の実装では、Walker という解析処理を行う構造体が、そのインスタンスごとに token.FileSet を保持していました。しかし、複数の Walker インスタンスが同じファイルセットを解析する場合、それぞれが独立した token.FileSet を持つことは冗長であり、メモリ使用量の増加や、場合によってはパフォーマンスの低下を招く可能性がありました。

この変更は、token.FileSet をグローバルに共有することで、これらの問題を解決し、将来的に cmd/api の解析ロジックがより効率的になるように基盤を整えることを目的としています。例えば、複数のパッケージやファイルを横断して解析を行う際に、共通の token.FileSet を利用することで、ファイル位置情報の管理が一元化され、より複雑な解析処理が容易になることが考えられます。

前提知識の解説

このコミットを理解するためには、Go言語の標準ライブラリにおける以下のパッケージと概念の理解が不可欠です。

  • go/token パッケージ:

    • token パッケージは、Goソースコードの字句解析(lexical analysis)において、トークン(識別子、キーワード、演算子など)とその位置情報を扱うための基本的な型を提供します。
    • token.FileSet は、Goソースファイル群の集合を表すデータ構造です。各ソースファイルは FileSet に追加され、そのファイル内の各トークンの位置(オフセット、行、列)が FileSet によって管理されます。これにより、コンパイラやツールがソースコード内の特定の位置を正確に参照できるようになります。FileSet は、複数のファイルにまたがる位置情報を一貫して扱うために使用されます。
    • token.Pos は、ソースコード内の位置を表す型です。これは FileSet 内のオフセットとして表現され、FileSet を介して実際の行番号や列番号に変換できます。
  • go/parser パッケージ:

    • parser パッケージは、Goソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築するための機能を提供します。
    • parser.ParseFile 関数は、指定された token.FileSet とファイルパス、およびその他のオプションを受け取り、Goソースファイルを解析して *ast.File(ASTのルートノード)を返します。この関数が FileSet を引数に取るのは、解析中に見つかったトークンの位置情報をその FileSet に登録するためです。
  • go/ast パッケージ:

    • ast パッケージは、Goソースコードの抽象構文木(AST)を表現するためのデータ構造を定義します。ASTは、ソースコードの構造を木構造で表現したもので、コンパイラやコード分析ツールがプログラムの意味を理解するために使用します。
  • go/printer パッケージ:

    • printer パッケージは、go/ast パッケージで構築されたASTをGoソースコードとして整形して出力するための機能を提供します。
    • printer.Fprint 関数は、指定された io.Writertoken.FileSet、およびASTノードを受け取り、ASTを整形して出力します。ここでも FileSet が必要となるのは、ASTノードが参照する位置情報(token.Pos)を実際の行番号や列番号に変換して、正確なソースコードを再構築するためです。

これらのパッケージは、Go言語のコンパイラ、リンター、フォーマッター、ドキュメンテーションジェネレーターなど、Goソースコードを扱う多くのツールで利用される基盤技術です。

技術的詳細

このコミットの技術的な核心は、token.FileSet のインスタンス管理の変更にあります。

変更前は、Walker 構造体が fset *token.FileSet フィールドを持ち、NewWalker 関数が呼ばれるたびに token.NewFileSet() を呼び出して新しい FileSet インスタンスを生成していました。これは、Walker の各インスタンスが独立した解析コンテキストを持つことを意味します。

// 変更前
type Walker struct {
    // ...
    fset            *token.FileSet
    // ...
}

func NewWalker() *Walker {
    return &Walker{
        fset:            token.NewFileSet(), // 各Walkerインスタンスが独自のFileSetを持つ
        // ...
    }
}

変更後は、fset フィールドが Walker 構造体から削除され、代わりに var fset = token.NewFileSet() というグローバル変数として token.FileSet のインスタンスが宣言されました。これにより、すべての Walker インスタンスが同じグローバルな FileSet を共有するようになります。

// 変更後
var fset = token.NewFileSet() // グローバルな単一のFileSet

type Walker struct {
    // ...
    // fset フィールドは削除された
    // ...
}

func NewWalker() *Walker {
    return &Walker{
        // fset の初期化は不要になった
        // ...
    }
}

この変更の技術的な影響は以下の通りです。

  1. メモリ使用量の削減: 複数の Walker インスタンスが作成される場合、それぞれが FileSet を持つ必要がなくなるため、全体的なメモリ使用量が削減されます。FileSet は、解析対象のすべてのファイルのメタデータ(ファイル名、サイズ、行オフセットなど)を保持するため、特に多数のファイルを解析するシナリオでは、この削減効果は大きくなります。
  2. 一貫性の向上: すべての解析操作が同じ FileSet を参照するため、ファイル内の位置情報の一貫性が保証されます。これは、異なる Walker インスタンスが同じファイルの一部を解析する場合でも、位置情報が正しくマッピングされることを意味します。
  3. 将来の拡張性: コミットメッセージにある「Prep for future CL」の通り、この変更は将来の機能拡張のための基盤となります。例えば、cmd/api が複数のパッケージを横断してAPIの変更を追跡するような、より複雑な解析を行う場合、単一の FileSet が存在することで、異なるパッケージのファイル間の参照や位置情報の解決が容易になります。
  4. 並行処理への影響: token.FileSet は、複数のゴルーチンから同時に読み取られることを想定して設計されており、内部的にミューテックスを使用して安全性を確保しています。したがって、グローバルな FileSet を使用しても、並行してファイルを解析する際に競合状態が発生するリスクは低いと考えられます。ただし、FileSet に新しいファイルを追加する操作は書き込み操作であり、この操作が頻繁に行われる場合は、グローバルな FileSet へのアクセスがボトルネックになる可能性も考慮する必要があります。しかし、cmd/api の文脈では、通常、解析対象のファイルは一度 FileSet に追加されれば、その後は主に読み取り操作が行われるため、大きな問題にはならないでしょう。

この変更は、cmd/api の内部構造をより効率的かつ堅牢にするための、クリーンアップと最適化の一環と見なすことができます。

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

--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -290,10 +290,11 @@ type pkgSymbol struct {\n \tsymbol string // \"RoundTripper\"\n }\n \n+var fset = token.NewFileSet()\n+\n type Walker struct {\n \tcontext         *build.Context\n \troot            string\n-\tfset            *token.FileSet\n \tscope           []string\n \tfeatures        map[string]bool // set\n \tlastConstType   string\n@@ -310,7 +311,6 @@ type Walker struct {\n \n func NewWalker() *Walker {\n \treturn &Walker{\n-\t\tfset:            token.NewFileSet(),\n \t\tfeatures:        make(map[string]bool),\n \t\tpackageState:    make(map[string]loadState),\n \t\tinterfaces:      make(map[pkgSymbol]*ast.InterfaceType),\n@@ -386,7 +386,7 @@ func (w *Walker) WalkPackage(name string) {\n \n \tfiles := append(append([]string{}, info.GoFiles...), info.CgoFiles...)\n \tfor _, file := range files {\n-\t\tf, err := parser.ParseFile(w.fset, filepath.Join(dir, file), nil, 0)\n+\t\tf, err := parser.ParseFile(fset, filepath.Join(dir, file), nil, 0)\n \t\tif err != nil {\n \t\t\tlog.Fatalf(\"error parsing package %s, file %s: %v\", name, file, err)\n \t\t}\n@@ -521,7 +521,7 @@ func (w *Walker) walkFile(file *ast.File) {\n \t\t\t// Ignore. Handled in subsequent pass, by go/doc.\n \t\tdefault:\n \t\t\tlog.Printf(\"unhandled %T, %#v\\n\", di, di)\n-\t\t\tprinter.Fprint(os.Stderr, w.fset, di)\n+\t\t\tprinter.Fprint(os.Stderr, fset, di)\n \t\t\tos.Stderr.Write([]byte(\"\\n\"))\n \t\t}\n \t}\n@@ -835,7 +835,7 @@ func (w *Walker) nodeString(node interface{}) string {\n \t\treturn \"\"\n \t}\n \tvar b bytes.Buffer\n-\tprinter.Fprint(&b, w.fset, node)\n+\tprinter.Fprint(&b, fset, node)\n \treturn b.String()\n }\n \n@@ -844,7 +844,7 @@ func (w *Walker) nodeDebug(node interface{}) string {\n \t\treturn \"\"\n \t}\n \tvar b bytes.Buffer\n-\tast.Fprint(&b, w.fset, node, nil)\n+\tast.Fprint(&b, fset, node, nil)\n \treturn b.String()\n }\n \n```

## コアとなるコードの解説

上記の差分は、`src/cmd/api/goapi.go` ファイルにおける `token.FileSet` の利用方法の変更を明確に示しています。

1.  **グローバル変数の導入**:
    ```go
    +var fset = token.NewFileSet()
    ```
    `Walker` 構造体の定義の前に、`fset` という名前のグローバル変数が導入され、`token.NewFileSet()` を呼び出して初期化されています。これにより、プログラム全体で共有される単一の `FileSet` インスタンスが作成されます。

2.  **`Walker` 構造体からの `fset` フィールドの削除**:
    ```diff
    -	fset            *token.FileSet
    ```
    `Walker` 構造体から `fset` フィールドが削除されました。これは、各 `Walker` インスタンスが独自の `FileSet` を持つ必要がなくなったことを意味します。

3.  **`NewWalker` 関数からの `fset` 初期化の削除**:
    ```diff
    -	t\tfset:            token.NewFileSet(),
    ```
    `NewWalker` 関数内で `fset` フィールドを初期化していた行が削除されました。これにより、`Walker` インスタンスが作成されるたびに新しい `FileSet` が生成されることがなくなります。

4.  **`parser.ParseFile` 呼び出しの変更**:
    ```diff
    -	t\tf, err := parser.ParseFile(w.fset, filepath.Join(dir, file), nil, 0)
    +	t\tf, err := parser.ParseFile(fset, filepath.Join(dir, file), nil, 0)
    ```
    `WalkPackage` メソッド内で `parser.ParseFile` を呼び出す際、以前は `w.fset`(`Walker` インスタンスに紐づく `FileSet`)を渡していましたが、変更後はグローバルな `fset` 変数を直接渡すようになっています。これにより、すべてのファイル解析が同じグローバルな `FileSet` に登録されるようになります。

5.  **`printer.Fprint` および `ast.Fprint` 呼び出しの変更**:
    ```diff
    -	t\tprinter.Fprint(os.Stderr, w.fset, di)
    +	t\tprinter.Fprint(os.Stderr, fset, di)
    // ...
    -	t\tprinter.Fprint(&b, w.fset, node)
    +	t\tprinter.Fprint(&b, fset, node)
    // ...
    -	t\tast.Fprint(&b, w.fset, node, nil)
    +	t\tast.Fprint(&b, fset, node, nil)
    ```
    `walkFile`, `nodeString`, `nodeDebug` メソッド内で `printer.Fprint` や `ast.Fprint` を呼び出す際も、同様に `w.fset` からグローバルな `fset` へと引数が変更されています。これらの関数は、ASTノードからソースコードを再構築したり、デバッグ情報を出力したりする際に、正確な位置情報を得るために `FileSet` を必要とします。グローバルな `FileSet` を使用することで、これらの操作も一貫した位置情報に基づいて行われるようになります。

これらの変更は、`cmd/api` ツールがGoソースコードを解析し、その構造を扱う方法の根本的な変更であり、`token.FileSet` の利用をより効率的かつ一元化されたものにしています。

## 関連リンク

*   **Go CL 6843048**: [https://golang.org/cl/6843048](https://golang.org/cl/6843048)

## 参考にした情報源リンク

*   Go言語 `go/token` パッケージドキュメント: [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
*   Go言語 `go/parser` パッケージドキュメント: [https://pkg.go.dev/go/parser](https://pkg.go.dev/go/parser)
*   Go言語 `go/ast` パッケージドキュメント: [https://pkg.go.dev/go/ast](https://pkg.go.dev/go/ast)
*   Go言語 `go/printer` パッケージドキュメント: [https://pkg.go.dev/go/printer](https://pkg.go.dev/go/printer)
*   Go言語のソースコード解析に関する一般的な情報 (例: "Go AST parsing"): (一般的なGoのAST解析に関するブログ記事やチュートリアルを参照)
    *   例: [https://go.dev/blog/go-ast-walk](https://go.dev/blog/go-ast-walk) (Go公式ブログのASTに関する記事)
    *   例: [https://yourbasic.org/golang/go-ast-example/](https://yourbasic.org/golang/go-ast-example/) (Go ASTの基本的な例)
    *   例: [https://medium.com/@matryer/understanding-the-go-ast-package-for-static-analysis-and-code-generation-c2a070c7e7d](https://medium.com/@matryer/understanding-the-go-ast-package-for-static-analysis-and-code-generation-c2a070c7e7d) (Go ASTパッケージの理解)
    *   (注: 上記のリンクは一般的な参考例であり、このコミットに直接関連するものではありませんが、前提知識の理解を深めるのに役立ちます。)