[インデックス 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/parser
、go/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
は、追加されたファイルのbase
とsize
に基づいて、次にAddFile
が呼び出されたときに使用されるBase()
値を更新します。
競合状態 (Race Condition)
競合状態とは、複数の並行に動作するプロセスやスレッド(この場合はGoのゴルーチン)が共有リソース(この場合はFileSet
の内部状態、特にBase()
値)にアクセスし、そのアクセス順序によって結果が非決定的に変わってしまう状態を指します。
このコミットの文脈では、以下のようなシナリオが競合状態を引き起こしていました。
- ゴルーチンAが
fset.Base()
を呼び出し、現在の基底オフセットB1
を取得する。 - その直後、ゴルーチンBが
fset.AddFile(...)
を呼び出し、FileSet
の内部状態(Base()
値)をB2
に更新する。 - ゴルーチンAが
fset.AddFile(..., B1, ...)
を呼び出す。しかし、B1
はもはや最新のBase()
値ではないため、ファイルの位置情報が他のファイルと重複したり、不正なオフセットになったりする可能性がある。
技術的詳細
このコミットの技術的な核心は、go/token/position.go
内のFileSet.AddFile
メソッドの変更にあります。
変更前は、AddFile
のbase
引数は常に非負の値である必要があり、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.init
がfset.Base()
を明示的に呼び出す代わりに-1
を渡すようになったことで、前述の競合状態のリスクが解消されました。parser
は、ファイルセットにファイルを追加する際に、常に最新の適切な基底オフセットが自動的に割り当てられることを期待できるようになります。
また、go/token/position_test.go
には、この新しい挙動を検証するためのテストケースが追加されています。TestFiles
関数内で、fset.AddFile
にfset.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である可能性があります。)
参考にした情報源リンク
- Go言語の
go/token
パッケージのドキュメント (Goの公式ドキュメント) - Go言語における並行処理と競合状態に関する一般的な知識
- GitHubのコミットページ: https://github.com/golang/go/commit/67acff0b09a187c56debc6cae23495ecc8ef3205
- Go Issue #5418に関するWeb検索結果 (直接的な情報は見つからず)