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

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

このコミットは、Go言語のgo/tokenパッケージにおけるFileSet.AddFile関数の挙動を改善するものです。具体的には、AddFile関数がbase引数として負の値を許容するように変更され、負の値が渡された場合にはFileSetの現在のBase()値が自動的に使用されるようになりました。これにより、上位レベルで発生していた競合状態(race condition)が修正され、より堅牢なファイルセット管理が可能になります。

コミット

commit 67acff0b09a187c56debc6cae23495ecc8ef3205
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Tue May 14 09:30:13 2013 -0700

    go/token: let FileSet.AddFile take a negative base
    
    Negative base now means "automatic". Fixes a higher
    level race.
    
    Fixes #5418
    
    R=gri
    CC=golang-dev
    https://golang.org/cl/9269043

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

https://github.com/golang/go/commit/67acff0b09a187c56debc6cae23495ecc8ef3205

元コミット内容

go/token: let FileSet.AddFile take a negative base

Negative base now means "automatic". Fixes a higher
level race.

Fixes #5418

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

変更の背景

この変更の背景には、Go言語のパーサーやツールがソースコードを処理する際に使用するgo/tokenパッケージのFileSetにおける、ファイル位置管理の競合状態がありました。

go/tokenパッケージは、Goのソースコードにおけるトークンや位置情報を管理するための基本的な機能を提供します。FileSetは、複数のソースファイルをまとめて管理し、各ファイル内の位置(オフセット)をグローバルな位置にマッピングする役割を担います。

従来のFileSet.AddFile関数は、新しいファイルを追加する際にbase(基底オフセット)とsize(ファイルサイズ)を明示的に指定する必要がありました。baseは、そのファイルがFileSet全体の中でどこから始まるかを示すオフセットです。通常、新しいファイルを追加する際には、FileSetの現在のBase()値(つまり、既存のファイルの末尾の次の位置)をbaseとして使用します。

しかし、複数のゴルーチン(または並行処理)が同時にFileSetにファイルを追加しようとするような、上位レベルのシナリオにおいて、このfset.Base()を呼び出してその値をAddFileに渡すというパターンが競合状態を引き起こす可能性がありました。具体的には、あるゴルーチンがfset.Base()を呼び出した直後に、別のゴルーチンが別のファイルを追加してfset.Base()の値を更新してしまうと、最初のゴルーチンがAddFileに渡すbaseが古くなり、結果としてファイルの位置情報が不正になる可能性がありました。

このコミットは、AddFileに負のbase値を渡すことで、FileSet自身が内部的に現在のBase()値を自動的に使用するようにすることで、この競合状態を回避することを目的としています。これにより、呼び出し元がfset.Base()を事前に取得する必要がなくなり、アトミックなファイル追加操作が可能になります。コミットメッセージにあるFixes #5418は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。

前提知識の解説

go/tokenパッケージ

go/tokenパッケージは、Go言語のソースコードを解析する際に、ファイル内の位置(行、列、オフセット)を表現し、管理するための基本的な型と関数を提供します。これは、Goコンパイラ、go/parsergo/printerなどのツールが内部的に利用する重要なパッケージです。

FileSet

FileSetは、複数のソースファイルをまとめて管理するための構造体です。Goのソースコードは通常複数のファイルに分かれており、FileSetはこれらのファイル全体にわたる一意な位置情報を提供します。各ファイルはFileSet内で連続したオフセット範囲を占めます。

FileSet.Base()

FileSet.Base()メソッドは、FileSetに次に追加されるファイルの開始オフセットとして推奨される値を返します。これは、現在FileSetに登録されている最後のファイルの末尾の次の位置に相当します。

FileSet.AddFile(filename string, base, size int) *File

このメソッドは、新しいファイルをFileSetに追加します。

  • filename: 追加するファイルのパスまたは名前。
  • base: ファイルがFileSet全体の中で始まるオフセット。
  • size: ファイルのバイトサイズ。

この関数は、追加されたファイルを表す*Fileオブジェクトを返します。FileSetは、追加されたファイルのbasesizeに基づいて、次にAddFileが呼び出されたときに使用されるBase()値を更新します。

競合状態 (Race Condition)

競合状態とは、複数の並行に動作するプロセスやスレッド(この場合はGoのゴルーチン)が共有リソース(この場合はFileSetの内部状態、特にBase()値)にアクセスし、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。

このコミットの文脈では、以下のようなシナリオが競合状態を引き起こしていました。

  1. ゴルーチンAがfset.Base()を呼び出し、現在の基底オフセットB1を取得する。
  2. その直後、ゴルーチンBがfset.AddFile(...)を呼び出し、FileSetの内部状態(Base()値)をB2に更新する。
  3. ゴルーチンAがfset.AddFile(..., B1, ...)を呼び出す。しかし、B1はもはや最新のBase()値ではないため、ファイルの位置情報が他のファイルと重複したり、不正なオフセットになったりする可能性がある。

技術的詳細

このコミットの技術的な核心は、go/token/position.go内のFileSet.AddFileメソッドの変更にあります。

変更前は、AddFilebase引数は常に非負の値である必要があり、FileSetの現在のBase()値よりも小さくてはならないという制約がありました。もしこれらの条件が満たされない場合、panicが発生しました。

変更後、AddFile関数はbase引数として負の値を特別に扱うようになりました。

func (s *FileSet) AddFile(filename string, base, size int) *File {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	if base < 0 { // ここが追加されたロジック
		base = s.base // 負の値の場合、FileSetの現在の内部base値を使用
	}
	if base < s.base || size < 0 {
		panic("illegal base or size")
	}
	// ... 既存のロジック ...
}

この変更により、baseに負の値(例: -1)が渡された場合、FileSetはロックを取得した状態で自身の内部的なs.baseフィールド(これはFileSet.Base()が返す値と同じ)をbaseとして使用するようになります。これにより、fset.Base()を呼び出してその値をAddFileに渡すという二段階の操作が不要になり、AddFileの呼び出し自体がアトミックに現在のFileSetの末尾にファイルを追加する操作として機能するようになります。

この修正は、go/parser/parser.goにも影響を与えています。parser.goはGoのソースコードを解析する際にgo/tokenパッケージを利用しており、parser.init関数内でFileSet.AddFileを呼び出しています。

変更前:

func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
	p.file = fset.AddFile(filename, fset.Base(), len(src))
	// ...
}

変更後:

func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
	p.file = fset.AddFile(filename, -1, len(src)) // baseに-1を渡すように変更
	// ...
}

parser.initfset.Base()を明示的に呼び出す代わりに-1を渡すようになったことで、前述の競合状態のリスクが解消されました。parserは、ファイルセットにファイルを追加する際に、常に最新の適切な基底オフセットが自動的に割り当てられることを期待できるようになります。

また、go/token/position_test.goには、この新しい挙動を検証するためのテストケースが追加されています。TestFiles関数内で、fset.AddFilefset.Base()を渡す場合と、-1を渡す場合の両方をテストし、どちらの場合も正しくファイルが追加され、位置情報が管理されることを確認しています。

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

src/pkg/go/parser/parser.go

--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -64,7 +64,7 @@ type parser struct {
 }
 
 func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
-	p.file = fset.AddFile(filename, fset.Base(), len(src))
+	p.file = fset.AddFile(filename, -1, len(src))
 	var m scanner.Mode
 	if mode&ParseComments != 0 {
 		m = scanner.ScanComments

src/pkg/go/token/position.go

--- a/src/pkg/go/token/position.go
+++ b/src/pkg/go/token/position.go
@@ -314,7 +314,8 @@ func (s *FileSet) Base() int {
 // AddFile adds a new file with a given filename, base offset, and file size
 // to the file set s and returns the file. Multiple files may have the same
 // name. The base offset must not be smaller than the FileSet's Base(), and
-// size must not be negative.
+// size must not be negative. As a special case, if a negative base is provided,
+// the current value of the FileSet's Base() is used instead.
 //
 // Adding the file will set the file set's Base() value to base + size + 1
 // as the minimum base value for the next file. The following relationship
@@ -329,6 +330,9 @@ func (s *FileSet) AddFile(filename string, base, size int) *File {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
+	if base < 0 {
+		base = s.base
+	}
 	if base < s.base || size < 0 {
 		panic("illegal base or size")
 	}

src/pkg/go/token/position_test.go

--- a/src/pkg/go/token/position_test.go
+++ b/src/pkg/go/token/position_test.go
@@ -167,7 +167,13 @@ func TestLineInfo(t *testing.T) {
 func TestFiles(t *testing.T) {
 	fset := NewFileSet()
 	for i, test := range tests {
-		fset.AddFile(test.filename, fset.Base(), test.size)
+		base := fset.Base()
+		if i%2 == 1 {
+			// Setting a negative base is equivalent to
+			// fset.Base(), so test some of each.
+			base = -1
+		}
+		fset.AddFile(test.filename, base, test.size)
 		j := 0
 		fset.Iterate(func(f *File) bool {
 			if f.Name() != tests[j].filename {

コアとなるコードの解説

src/pkg/go/token/position.go の変更

  • FileSet.AddFile関数のシグネチャは変更されていませんが、内部ロジックが追加されました。
  • if base < 0 { base = s.base } という行が追加されました。これは、base引数が負の値である場合に、FileSetの内部状態であるs.base(現在のファイルセットの末尾の次のオフセット)をbaseとして使用することを意味します。これにより、呼び出し元が明示的にfset.Base()を呼び出す必要がなくなり、AddFileが自動的に適切なオフセットを決定できるようになります。
  • コメントも更新され、「As a special case, if a negative base is provided, the current value of the FileSet's Base() is used instead.」と追記され、この新しい挙動が明記されました。

src/pkg/go/parser/parser.go の変更

  • parser.init関数内でfset.AddFileを呼び出す箇所が変更されました。
  • 以前はfset.AddFile(filename, fset.Base(), len(src))のように、fset.Base()を明示的に呼び出してその結果をbase引数に渡していました。
  • 変更後はfset.AddFile(filename, -1, len(src))となり、base引数に-1を直接渡すようになりました。これにより、FileSet.AddFileの新しいロジックが適用され、内部で自動的に適切なbase値が設定されるため、競合状態のリスクが排除されます。

src/pkg/go/token/position_test.go の変更

  • TestFiles関数に、FileSet.AddFileの新しい挙動を検証するためのテストロジックが追加されました。
  • ループ内でi%2 == 1(つまり、2回に1回)の場合にbase-1に設定し、それ以外の場合はfset.Base()を明示的に使用するようにしています。
  • これにより、baseに負の値を渡した場合と、明示的にfset.Base()を渡した場合の両方で、FileSetが正しく動作し、ファイルの位置情報が期待通りに管理されることが確認されます。これは、新しい機能が既存の機能と互換性を持ち、かつ正しく動作することを保証するための重要なテストです。

関連リンク

  • Go CL (Code Review) リンク: https://golang.org/cl/9269043
  • 関連するGo Issue: #5418 (Goの公式GitHubリポジトリでは直接この番号のIssueは見つかりませんでしたが、コミットメッセージに記載されているため、内部的なトラッカーまたは非常に古いクローズされたIssueである可能性があります。)

参考にした情報源リンク