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

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

このコミットは、Go言語の公式フォーマッタである gofmt コマンドにおける、長期実行テスト(long test)での競合状態(race condition)を修正するものです。具体的には、gofmt が内部的に使用する go/token.FileSet の扱いを改善し、テストの安定性を向上させています。

コミット

commit dfb1af4b97ffed0c2acbbc52b0f85355d727974a
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu Mar 8 23:56:26 2012 +0900

    cmd/gofmt: fix race in long test

    Fixes #3249.

    R=rsc
    CC=golang-dev
    https://golang.org/cl/5792043

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

https://github.com/golang/go/commit/dfb1af4b97ffed0c2acbbc52b0f85355d727974a

元コミット内容

cmd/gofmt: fix race in long test Fixes #3249.

このコミットは、gofmt コマンドの長期テストにおける競合状態を修正し、Issue #3249 を解決することを目的としています。

変更の背景

gofmt はGo言語のソースコードを整形するためのツールであり、その正確性と安定性はGo開発において非常に重要です。このコミットが行われた当時、gofmt の長期実行テストにおいて、稀に失敗する競合状態の問題が存在していました。

競合状態とは、複数のゴルーチン(Goにおける軽量スレッド)が共有リソースに同時にアクセスし、そのアクセス順序によって結果が非決定的に変わってしまうバグの一種です。この場合、gofmt が内部で利用する go/token.FileSet オブジェクトが、テスト実行中に複数の処理から同時に変更される可能性があり、それがテストの不安定性を引き起こしていました。

Issue #3249 は、gofmt//line コメントを適切に処理しないという問題に関連している可能性があります。//line コメントは、コンパイラやデバッガに対して、ソースコードの行番号やファイル名を変更したかのように見せかけるための特殊なコメントです。gofmt がコードを整形する際に、これらのコメントの位置や内容を誤って変更してしまうと、デバッグ情報が狂ったり、コンパイルエラーが発生したりする可能性があります。競合状態がこの //line コメントの処理に影響を与え、テストが失敗していたと考えられます。

このコミットは、テストの信頼性を高め、gofmt の品質を保証するために必要とされました。

前提知識の解説

Go言語の go/token パッケージと FileSet

Go言語の標準ライブラリには、ソースコードの解析(パース)を支援するための go/token パッケージが含まれています。このパッケージの主要な型の一つが token.FileSet です。

  • token.FileSet: Goのソースコードを解析する際に、ファイル内の位置情報(行番号、列番号、オフセットなど)を管理するためのオブジェクトです。Goのパーサー(go/parser パッケージ)は、ソースコードを読み込む際に FileSet を使用して、各トークンやASTノードがソースコードのどの位置に存在するかを記録します。これにより、エラーメッセージの表示やデバッグ情報の生成が正確に行われます。
  • 位置情報の一元管理: FileSet は、複数のソースファイルにまたがる位置情報を一元的に管理します。例えば、複数のファイルからなるパッケージを解析する場合、すべてのファイルが同じ FileSet を共有することで、ファイル間の参照やエラー報告が正確になります。
  • スレッドセーフティ: FileSet は、その性質上、複数のパーシング処理から同時にアクセスされる可能性があります。特に並行処理を行うアプリケーションやテストにおいては、FileSet の操作がスレッドセーフであるか、あるいは適切に同期されているかが重要になります。

競合状態(Race Condition)

競合状態は、並行処理において複数の処理(この場合はゴルーチン)が共有リソースにアクセスする際に発生する問題です。

  • 共有リソース: 複数の処理から読み書きされる可能性のあるデータやオブジェクト。このコミットの文脈では、token.FileSet が共有リソースに該当します。
  • 非決定性: 競合状態が発生すると、処理の実行順序によって最終的な結果が変わってしまうため、プログラムの動作が非決定性になります。これにより、バグの再現が困難になり、デバッグが非常に難しくなります。
  • データ競合(Data Race): 特に、複数の処理が同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に発生する競合状態をデータ競合と呼びます。Go言語では、go run -race コマンドでデータ競合を検出するツールが提供されています。

gofmt の動作原理

gofmt は、Goのソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築します。その後、このASTを標準的なGoのフォーマットルールに従って再構築し、整形されたコードを出力します。このプロセスにおいて、go/token.FileSet はASTノードの正確な位置情報を保持するために不可欠な役割を果たします。

技術的詳細

このコミットの核心は、gofmttoken.FileSet をどのように利用するかという点にあります。変更前は、gofmt のメイン処理内で fset = token.NewFileSet() という形でグローバル変数として FileSet が宣言され、初期化されていました。これは、gofmt プロセス全体で単一の FileSet インスタンスが共有されることを意味します。

通常の gofmt の実行では、単一のファイルまたはディレクトリを処理するため、このグローバルな FileSet の利用は問題になりにくいです。しかし、テスト、特に複数のファイルを並行して処理するような「長期テスト(long test)」においては、このグローバルな FileSet が複数のテストゴルーチンから同時にアクセスされ、変更される可能性がありました。

token.FileSet は、ファイルを追加したり、位置情報を更新したりする際に内部状態を変更します。複数のゴルーチンが同時に FileSet を変更しようとすると、データ競合が発生し、FileSet の内部状態が破損したり、不正な位置情報が生成されたりする可能性があります。これが、テストが不安定になる原因でした。

このコミットでは、この問題を解決するために以下の変更を行っています。

  1. グローバル FileSet の名称変更とコメント追加: fset というグローバル変数を fileSet に変更し、// per process FileSet というコメントを追加しています。これは、この FileSet がプロセス全体で共有されるものであることを明示しています。しかし、この変更だけでは競合状態は解決しません。

  2. parse 関数のシグネチャ変更: parse 関数は、ソースコードを解析してASTを生成する主要な関数です。変更前は parse(filename string, src []byte, stdin bool) でしたが、変更後は parse(fset *token.FileSet, filename string, src []byte, stdin bool) となり、token.FileSet のポインタを引数として受け取るようになりました。これにより、parse 関数がどの FileSet を使用するかを呼び出し元が明示的に指定できるようになります。

  3. processFile 関数での fileSet の引き渡し: gofmt のメイン処理である processFile 関数内で、parse 関数や ast.SortImportsprinter.Config.Fprint といった FileSet を必要とする処理に対して、グローバルな fileSet 変数を明示的に引き渡すように変更されました。これにより、gofmt の通常の実行パスでは引き続き単一の FileSet が使用されます。

  4. long_test.go での FileSet のローカル化: 最も重要な変更は src/cmd/gofmt/long_test.go にあります。

    • gofmt テストヘルパー関数も gofmt(fset *token.FileSet, filename string, src *bytes.Buffer)FileSet を引数として受け取るように変更されました。
    • testFile 関数内で、各テストケースの実行時に fset := token.NewFileSet() と、新しい FileSet インスタンスをローカルに作成するように変更されました。
    • このローカルに作成された fset が、parse 関数や gofmt ヘルパー関数に引き渡されるようになりました。

この変更により、long_test.go の各テストゴルーチンは、それぞれ独立した token.FileSet インスタンスを持つことになります。これにより、複数のゴルーチンが同時に FileSet を操作しても、互いの状態に影響を与えることがなくなり、競合状態が解消されます。

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

src/cmd/gofmt/gofmt.go

--- a/src/cmd/gofmt/gofmt.go
+++ b/src/cmd/gofmt/gofmt.go
@@ -41,7 +41,7 @@ var (
 )
 
 var (
-	fset        = token.NewFileSet()\n+\tfileSet     = token.NewFileSet() // per process FileSet\n \texitCode    = 0\n \trewrite     func(*ast.File) *ast.File\n \tparserMode  parser.Mode\n@@ -98,7 +98,7 @@ func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error\n \t\treturn err\n \t}\n \n-\tfile, adjust, err := parse(filename, src, stdin)\n+\tfile, adjust, err := parse(fileSet, filename, src, stdin)\n \tif err != nil {\n \t\treturn err\n \t}\n@@ -111,14 +111,14 @@ func processFile(filename string, in io.Reader, out io.Writer, stdin bool) error\n \t\t}\n \t}\n \n-\tast.SortImports(fset, file)\n+\tast.SortImports(fileSet, file)\n \n \tif *simplifyAST {\n \t\tsimplify(file)\n \t}\n \n \tvar buf bytes.Buffer\n-\terr = (&printer.Config{Mode: printerMode, Tabwidth: *tabWidth}).Fprint(&buf, fset, file)\n+\terr = (&printer.Config{Mode: printerMode, Tabwidth: *tabWidth}).Fprint(&buf, fileSet, file)\n \tif err != nil {\n \t\treturn err\n \t}\n@@ -254,7 +254,7 @@ func diff(b1, b2 []byte) (data []byte, err error) {\n \n // parse parses src, which was read from filename,\n // as a Go source file or statement list.\n-func parse(filename string, src []byte, stdin bool) (*ast.File, func(orig, src []byte) []byte, error) {\n+func parse(fset *token.FileSet, filename string, src []byte, stdin bool) (*ast.File, func(orig, src []byte) []byte, error) {\n \t// Try as whole source file.\n \tfile, err := parser.ParseFile(fset, filename, src, parserMode)\n \tif err == nil {\n```

### `src/cmd/gofmt/long_test.go`

```diff
--- a/src/cmd/gofmt/long_test.go
+++ b/src/cmd/gofmt/long_test.go
@@ -14,6 +14,7 @@ import (\n \t\"fmt\"\n \t\"go/ast\"\n \t\"go/printer\"\n+\t\"go/token\"\n \t\"io\"\n \t\"os\"\n \t\"path/filepath\"\n@@ -30,8 +31,8 @@ var (\n \tnfiles  int // number of files processed\n )\n \n-func gofmt(filename string, src *bytes.Buffer) error {\n-\tf, _, err := parse(filename, src.Bytes(), false)\n+\tfunc gofmt(fset *token.FileSet, filename string, src *bytes.Buffer) error {\n+\tf, _, err := parse(fset, filename, src.Bytes(), false)\n \tif err != nil {\n \t\treturn err\n \t}\n@@ -58,7 +59,8 @@ func testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string) {\n \t}\n \n \t// exclude files w/ syntax errors (typically test cases)\n-\tif _, _, err = parse(filename, b1.Bytes(), false); err != nil {\n+\tfset := token.NewFileSet()\n+\tif _, _, err = parse(fset, filename, b1.Bytes(), false); err != nil {\n \t\tif *verbose {\n \t\t\tfmt.Fprintf(os.Stderr, \"ignoring %s\\n\", err)\n \t\t}\n@@ -66,7 +68,7 @@ func testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string) {\n \t}\n \n \t// gofmt file\n-\tif err = gofmt(filename, b1); err != nil {\n+\tif err = gofmt(fset, filename, b1); err != nil {\n \t\tt.Errorf(\"1st gofmt failed: %v\", err)\n \t\treturn\n \t}\n@@ -76,7 +78,7 @@ func testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string) {\n \tb2.Write(b1.Bytes())\n \n \t// gofmt result again\n-\tif err = gofmt(filename, b2); err != nil {\n+\tif err = gofmt(fset, filename, b2); err != nil {\n \t\tt.Errorf(\"2nd gofmt failed: %v\", err)\n \t\treturn\n \t}\n```

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

### `src/cmd/gofmt/gofmt.go` の変更点

1.  **`fset` から `fileSet` への変数名変更**:
    `var ( fset = token.NewFileSet() )` が `var ( fileSet = token.NewFileSet() // per process FileSet )` に変更されました。これは単なる変数名の変更ですが、コメント `// per process FileSet` が追加されたことで、この `FileSet` がプロセス全体で共有されるものであるという意図が明確になりました。

2.  **`parse` 関数のシグネチャ変更**:
    `func parse(filename string, src []byte, stdin bool)` が `func parse(fset *token.FileSet, filename string, src []byte, stdin bool)` に変更されました。これにより、`parse` 関数がどの `FileSet` インスタンスを使用するかを呼び出し元が指定できるようになりました。これは、依存性注入(Dependency Injection)の一種と見なすことができ、関数の再利用性やテスト容易性を向上させます。

3.  **`processFile` 関数内での `fileSet` の引き渡し**:
    `processFile` 関数内で `parse`、`ast.SortImports`、`printer.Config.Fprint` の各呼び出しにおいて、グローバル変数 `fileSet` が明示的に引数として渡されるようになりました。これにより、`gofmt` の通常の実行では引き続き単一の `FileSet` が使用されます。

### `src/cmd/gofmt/long_test.go` の変更点

1.  **`go/token` パッケージのインポート**:
    `import ("go/token")` が追加され、`token.FileSet` 型が利用可能になりました。

2.  **`gofmt` テストヘルパー関数のシグネチャ変更**:
    `func gofmt(filename string, src *bytes.Buffer) error` が `func gofmt(fset *token.FileSet, filename string, src *bytes.Buffer) error` に変更されました。これにより、テスト用の `gofmt` ヘルパー関数も `FileSet` を引数として受け取るようになりました。

3.  **`testFile` 関数内での `FileSet` のローカル化**:
    `testFile` 関数内で、`parse` 関数を呼び出す前に `fset := token.NewFileSet()` という行が追加されました。これにより、各テストケースの実行ごとに新しい `FileSet` インスタンスが作成され、そのテストケース専用の `FileSet` が使用されるようになります。
    *   `if _, _, err = parse(filename, b1.Bytes(), false); err != nil {` が `if _, _, err = parse(fset, filename, b1.Bytes(), false); err != nil {` に変更。
    *   `if err = gofmt(filename, b1); err != nil {` が `if err = gofmt(fset, filename, b1); err != nil {` に変更。
    *   `if err = gofmt(filename, b2); err != nil {` が `if err = gofmt(fset, filename, b2); err != nil {` に変更。

この `long_test.go` での `FileSet` のローカル化が、競合状態の根本的な解決策です。各テストゴルーチンが独自の `FileSet` を持つことで、共有リソースへの同時書き込みがなくなり、テストの実行が安定します。

## 関連リンク

*   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)

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

*   GitHubコミットページ: [https://github.com/golang/go/commit/dfb1af4b97ffed0c2acbbc52b0f85355d727974a](https://github.com/golang/go/commit/dfb1af4b97ffed0c2acbbc52b0f85355d727974a)
*   Go Issue #3249: `cmd/gofmt: valid //line comments must remain at the beginning of the line` (Web検索結果より)
    *   このIssueの具体的な内容は、`gofmt` が `//line` コメントを適切に扱わないことによる問題を示唆しており、競合状態がその一因であった可能性が高いです。
*   Go言語の競合状態検出(Race Detector)に関するドキュメント: [https://go.dev/doc/articles/race_detector](https://go.dev/doc/articles/race_detector)