[インデックス 13537] ファイルの概要
このコミットは、Go言語の標準ライブラリ text/template/parse
パッケージにおけるデータ競合(data race)を修正するものです。具体的には、テンプレートの字句解析(lexing)を行う lexer
において、診断情報(エラーメッセージの行番号など)の計算時に発生するデータ競合を解消します。この修正は、診断情報にのみ影響し、APIの変更はありません。
コミット
commit 20d9fd3ae18bd5f80c3c0f8f424ebd9a72b6788a
Author: Rob Pike <r@golang.org>
Date: Mon Jul 30 15:11:20 2012 -0700
text/template/parse: fix data race
The situation only affects diagnostics but is easy to fix.
When computing lineNumber, use the position of the last item
returned by nextItem rather than the current state of the lexer.
This is internal only and does not affect the API.
Fixes #3886.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/6445061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/20d9fd3ae18bd5f80c3c0f8f424ebd9a72b6788a
元コミット内容
text/template/parse: fix data race
The situation only affects diagnostics but is easy to fix.
When computing lineNumber, use the position of the last item
returned by nextItem rather than the current state of the lexer.
This is internal only and does not affect the API.
Fixes #3886.
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/6445061
変更の背景
このコミットは、Go言語のIssue #3886で報告された問題に対応しています。text/template/parse
パッケージは、Goのテンプレートエンジンの中核をなす部分であり、テンプレート文字列を解析して構文木を構築します。この解析プロセスにおいて、字句解析器(lexer)がトークン(item
)を生成し、そのトークンに基づいて行番号などの診断情報を計算します。
問題は、lineNumber
を計算する際に、lexer
の現在の状態(l.pos
)を直接参照していたことにありました。lexer
は並行処理される可能性があり、l.pos
が複数のゴルーチンから同時にアクセスされると、データ競合が発生する可能性があります。このデータ競合は、プログラムのクラッシュや誤った動作を引き起こす可能性があり、特に診断情報(エラーメッセージの行番号など)の正確性に影響を与えます。
コミットメッセージにある「The situation only affects diagnostics but is easy to fix.」という記述は、このデータ競合がテンプレートの実行結果そのものに直接的な影響を与えるわけではなく、主にエラー報告やデバッグ時の行番号表示に問題を引き起こすことを示唆しています。しかし、診断情報の正確性は開発者にとって非常に重要であり、デバッグの困難さにつながるため、修正が必要とされました。
前提知識の解説
データ競合 (Data Race)
データ競合とは、複数の並行に実行されるスレッド(Goにおいてはゴルーチン)が、同期メカニズムなしに同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する競合状態です。データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュ、不正なデータ、セキュリティ脆弱性などの問題を引き起こす可能性があります。Go言語では、go run -race
コマンドでデータ競合を検出するツールが提供されています。
字句解析器 (Lexer/Scanner)
字句解析器(lexerまたはscanner)は、コンパイラやインタプリタの最初の段階で、入力された文字列(ソースコードなど)を意味のある最小単位であるトークン(token)の並びに分解するプログラムです。例えば、1 + 2
という文字列は、1
(数値トークン)、+
(演算子トークン)、2
(数値トークン)というトークンに分解されます。
text/template
パッケージ
Go言語の text/template
パッケージは、テキストベースのテンプレートを生成するための機能を提供します。これは、HTML、XML、プレーンテキストなどの動的なコンテンツを生成する際に非常に便利です。テンプレートは、プレースホルダーや制御構造(条件分岐、ループなど)を含むテキストであり、データが適用されると最終的な出力が生成されます。
lineNumber
の役割
テンプレートの解析中にエラーが発生した場合、そのエラーがテンプレートのどの行で発生したかを正確に報告することは、開発者にとってデバッグの助けとなります。lineNumber
は、このエラーが発生した行番号を計算するために使用されます。
item
と itemType
text/template/parse
パッケージでは、字句解析器が生成するトークンを item
構造体で表現します。itemType
は、そのトークンの種類(例: itemText
、itemLeftDelim
、itemNumber
など)を定義する列挙型です。
技術的詳細
このデータ競合は、lexer
構造体の lineNumber()
メソッドが l.pos
(現在の入力文字列における字句解析器の位置) を直接使用して行番号を計算していたために発生しました。l.pos
は lexer
の内部状態であり、nextItem()
メソッドが l.items
チャンネルからアイテムを読み取る際に、l.pos
が更新される可能性があります。複数のゴルーチンが nextItem()
を呼び出すような状況では、lineNumber()
が l.pos
を読み取るタイミングと、別のゴルーチンが l.pos
を書き込むタイミングが競合し、不正な行番号が計算される可能性がありました。
修正の核心は、lineNumber
の計算を lexer
の現在の状態に依存するのではなく、nextItem
によって返された最後の item
の位置に依存するように変更することです。これにより、lineNumber
の計算が、すでに確定したトークンの位置に基づいて行われるため、データ競合が解消されます。
具体的には、以下の変更が行われました。
-
item
構造体へのpos
フィールドの追加:item
構造体にpos int
フィールドが追加されました。これは、そのトークンが入力文字列のどこから始まったかを示すバイトオフセットです。type item struct { typ itemType // The type of this item. pos int // The starting position, in bytes, of this item in the input string. val string // The value of this item. }
-
lexer
構造体へのlastPos
フィールドの追加:lexer
構造体にlastPos int
フィールドが追加されました。これは、nextItem
メソッドが最後に返したitem
の開始位置を保持します。type lexer struct { // ... lastPos int // position of nost recent item returned by nextItem // ... }
-
emit
メソッドの変更:emit
メソッドは、item
をl.items
チャンネルに送信する際に、新しいpos
フィールドにl.start
(現在のトークンの開始位置) を設定するように変更されました。func (l *lexer) emit(t itemType) { l.items <- item{t, l.start, l.input[l.start:l.pos]} // l.start を pos として渡す l.start = l.pos }
-
errorf
メソッドの変更:errorf
メソッドも同様に、エラーアイテムを生成する際にl.start
をpos
として渡すように変更されました。func (l *lexer) errorf(format string, args ...interface{}) stateFn { l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} // l.start を pos として渡す return nil }
-
nextItem
メソッドの変更:nextItem
メソッドは、l.items
チャンネルからitem
を受け取った後、そのitem
のpos
をl.lastPos
に保存するように変更されました。これにより、lineNumber
が常に最後に処理されたトークンの位置に基づいて計算されるようになります。func (l *lexer) nextItem() item { item := <-l.items l.lastPos = item.pos // ここで lastPos を更新 return item }
-
lineNumber
メソッドの変更:lineNumber
メソッドは、l.pos
の代わりにl.lastPos
を使用して行番号を計算するように変更されました。func (l *lexer) lineNumber() int { return 1 + strings.Count(l.input[:l.lastPos], "\\n") // l.lastPos を使用 }
これらの変更により、lineNumber
の計算は、nextItem
がすでに処理し終えたトークンの位置に依存するようになり、lexer
の内部状態の同時変更によるデータ競合が回避されます。
テストコード (lex_test.go
) も、item
構造体に pos
フィールドが追加されたことに伴い、既存のテストケースの item
の初期化に 0
を pos
の値として追加する変更が行われました。さらに、equal
関数が導入され、reflect.DeepEqual
の代わりに item
の比較を行うようになりました。これは、pos
フィールドの比較をオプションにするためです。そして、TestPos
という新しいテスト関数が追加され、item
の pos
フィールドが正しく設定されていることを明示的に検証するようになりました。
コアとなるコードの変更箇所
src/pkg/text/template/parse/lex.go
--- a/src/pkg/text/template/parse/lex.go
+++ b/src/pkg/text/template/parse/lex.go
@@ -13,8 +13,9 @@ import (
// item represents a token or text string returned from the scanner.
type item struct {
- typ itemType
- val string
+ typ itemType // The type of this item.
+ pos int // The starting position, in bytes, of this item in the input string.
+ val string // The value of this item.
}
func (i item) String() string {
@@ -127,6 +128,7 @@ type lexer struct {
pos int // current position in the input.
start int // start position of this item.
width int // width of last rune read from input.
+ lastPos int // position of nost recent item returned by nextItem
items chan item // channel of scanned items.
}
@@ -155,7 +157,7 @@ func (l *lexer) backup() {
// emit passes an item back to the client.
func (l *lexer) emit(t itemType) {
- l.items <- item{t, l.input[l.start:l.pos]}
+ l.items <- item{t, l.start, l.input[l.start:l.pos]}
l.start = l.pos
}
@@ -180,22 +182,25 @@ func (l *lexer) acceptRun(valid string) {
l.backup()
}
-// lineNumber reports which line we're on. Doing it this way
+// lineNumber reports which line we're on, based on the position of
+// the previous item returned by nextItem. Doing it this way
// means we don't have to worry about peek double counting.
func (l *lexer) lineNumber() int {
- return 1 + strings.Count(l.input[:l.pos], "\\n")
+ return 1 + strings.Count(l.input[:l.lastPos], "\\n")
}
// error returns an error token and terminates the scan by passing
// back a nil pointer that will be the next state, terminating l.nextItem.
func (l *lexer) errorf(format string, args ...interface{}) stateFn {
- l.items <- item{itemError, fmt.Sprintf(format, args...)}
+ l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}
return nil
}
// nextItem returns the next item from the input.
func (l *lexer) nextItem() item {
- return <-l.items
+ item := <-l.items
+ l.lastPos = item.pos
+ return item
}
// lex creates a new scanner for the input string.
src/pkg/text/template/parse/lex_test.go
--- a/src/pkg/text/template/parse/lex_test.go
+++ b/src/pkg/text/template/parse/lex_test.go
@@ -5,7 +5,6 @@
package parse
import (
- "reflect"
"testing"
)
@@ -16,31 +15,31 @@ type lexTest struct {
}\n \n var (\n-\ttEOF = item{itemEOF, ""}\n-\ttLeft = item{itemLeftDelim, "{{"}\n-\ttRight = item{itemRightDelim, "}}""}\n-\ttRange = item{itemRange, "range"}\n-\ttPipe = item{itemPipe, "|"}\n-\ttFor = item{itemIdentifier, "for"}\n-\ttQuote = item{itemString, `"abc \\n\\t\\\" "`}\n+\ttEOF = item{itemEOF, 0, ""}\n+\ttLeft = item{itemLeftDelim, 0, "{{"}\n+\ttRight = item{itemRightDelim, 0, "}}""}\n+\ttRange = item{itemRange, 0, "range"}\n+\ttPipe = item{itemPipe, 0, "|"}\n+\ttFor = item{itemIdentifier, 0, "for"}\n+\ttQuote = item{itemString, 0, `"abc \\n\\t\\\" "`}\n \traw = "`" + `abc\\n\\t\\\" ` + "`"\n-\ttRawQuote = item{itemRawString, raw}\n+\ttRawQuote = item{itemRawString, 0, raw}\n )\n \n var lexTests = []lexTest{\n \t{"empty", "", []item{tEOF}},\n-\t{"spaces", " \\t\\n", []item{{itemText, " \\t\\n"}, tEOF}},\n-\t{"text", `now is the time`, []item{{itemText, "now is the time"}, tEOF}},\n+\t{"spaces", " \\t\\n", []item{{itemText, 0, " \\t\\n"}, tEOF}},\n+\t{"text", `now is the time`, []item{{itemText, 0, "now is the time"}, tEOF}},\n \t{"text with comment", "hello-{{/* this is a comment */}}-world", []item{\n-\t\t{itemText, "hello-"},\n-\t\t{itemText, "-world"},\n+\t\t{itemText, 0, "hello-"},\n+\t\t{itemText, 0, "-world"},\n \t\ttEOF,\n \t}},\n \t{"punctuation", "{{,@%}}", []item{\n \t\ttLeft,\n-\t\t{itemChar, ","},\n-\t\t{itemChar, "@"},\n-\t\t{itemChar, "%"},\n+\t\t{itemChar, 0, ","},\n+\t\t{itemChar, 0, "@"},\n+\t\t{itemChar, 0, "%"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n@@ -50,139 +49,139 @@ var lexTests = []lexTest{\n \t{"raw quote", "{{" + raw + "}}", []item{tLeft, tRawQuote, tRight, tEOF}},\n \t{"numbers", "{{1 02 0x14 -7.2i 1e3 +1.2e-4 4.2i 1+2i}}", []item{\n \t\ttLeft,\n-\t\t{itemNumber, "1"},\n-\t\t{itemNumber, "02"},\n-\t\t{itemNumber, "0x14"},\n-\t\t{itemNumber, "-7.2i"},\n-\t\t{itemNumber, "1e3"},\n-\t\t{itemNumber, "+1.2e-4"},\n-\t\t{itemNumber, "4.2i"},\n-\t\t{itemComplex, "1+2i"},\n+\t\t{itemNumber, 0, "1"},\n+\t\t{itemNumber, 0, "02"},\n+\t\t{itemNumber, 0, "0x14"},\n+\t\t{itemNumber, 0, "-7.2i"},\n+\t\t{itemNumber, 0, "1e3"},\n+\t\t{itemNumber, 0, "+1.2e-4"},\n+\t\t{itemNumber, 0, "4.2i"},\n+\t\t{itemComplex, 0, "1+2i"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"characters", `{{'a' '\\n' '\\'' '\\\\' '\\u00FF' '\\xFF' '本'}}`, []item{\n \t\ttLeft,\n-\t\t{itemCharConstant, `'a'`},\n-\t\t{itemCharConstant, `'\\n'`},\n-\t\t{itemCharConstant, `'\\''`},\n-\t\t{itemCharConstant, `'\\\\'`},\n-\t\t{itemCharConstant, `'\\u00FF'`},\n-\t\t{itemCharConstant, `'\\xFF'`},\n-\t\t{itemCharConstant, `'本'`},\n+\t\t{itemCharConstant, 0, `'a'`},\n+\t\t{itemCharConstant, 0, `'\\n'`},\n+\t\t{itemCharConstant, 0, `'\\''`},\n+\t\t{itemCharConstant, 0, `'\\\\'`},\n+\t\t{itemCharConstant, 0, `'\\u00FF'`},\n+\t\t{itemCharConstant, 0, `'\\xFF'`},\n+\t\t{itemCharConstant, 0, `'本'`},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"bools", "{{true false}}", []item{\n \t\ttLeft,\n-\t\t{itemBool, "true"},\n-\t\t{itemBool, "false"},\n+\t\t{itemBool, 0, "true"},\n+\t\t{itemBool, 0, "false"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"dot", "{{.}}", []item{\n \t\ttLeft,\n-\t\t{itemDot, "."},\n+\t\t{itemDot, 0, "."},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"dots", "{{.x . .2 .x.y}}", []item{\n \t\ttLeft,\n-\t\t{itemField, ".x"},\n-\t\t{itemDot, "."},\n-\t\t{itemNumber, ".2"},\n-\t\t{itemField, ".x.y"},\n+\t\t{itemField, 0, ".x"},\n+\t\t{itemDot, 0, "."},\n+\t\t{itemNumber, 0, ".2"},\n+\t\t{itemField, 0, ".x.y"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"keywords", "{{range if else end with}}", []item{\n \t\ttLeft,\n-\t\t{itemRange, "range"},\n-\t\t{itemIf, "if"},\n-\t\t{itemElse, "else"},\n-\t\t{itemEnd, "end"},\n-\t\t{itemWith, "with"},\n+\t\t{itemRange, 0, "range"},\n+\t\t{itemIf, 0, "if"},\n+\t\t{itemElse, 0, "else"},\n+\t\t{itemEnd, 0, "end"},\n+\t\t{itemWith, 0, "with"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"variables", "{{$c := printf $ $hello $23 $ $var.Field .Method}}", []item{\n \t\ttLeft,\n-\t\t{itemVariable, "$c"},\n-\t\t{itemColonEquals, ":="},\n-\t\t{itemIdentifier, "printf"},\n-\t\t{itemVariable, "$"},\n-\t\t{itemVariable, "$hello"},\n-\t\t{itemVariable, "$23"},\n-\t\t{itemVariable, "$"},\n-\t\t{itemVariable, "$var.Field"},\n-\t\t{itemField, ".Method"},\n+\t\t{itemVariable, 0, "$c"},\n+\t\t{itemColonEquals, 0, ":="},\n+\t\t{itemIdentifier, 0, "printf"},\n+\t\t{itemVariable, 0, "$"},\n+\t\t{itemVariable, 0, "$hello"},\n+\t\t{itemVariable, 0, "$23"},\n+\t\t{itemVariable, 0, "$"},\n+\t\t{itemVariable, 0, "$var.Field"},\n+\t\t{itemField, 0, ".Method"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"pipeline", `intro {{echo hi 1.2 |noargs|args 1 "hi"}} outro`, []item{\n-\t\t{itemText, "intro "},\n+\t\t{itemText, 0, "intro "},\n \t\ttLeft,\n-\t\t{itemIdentifier, "echo"},\n-\t\t{itemIdentifier, "hi"},\n-\t\t{itemNumber, "1.2"},\n+\t\t{itemIdentifier, 0, "echo"},\n+\t\t{itemIdentifier, 0, "hi"},\n+\t\t{itemNumber, 0, "1.2"},\n \t\ttPipe,\n-\t\t{itemIdentifier, "noargs"},\n+\t\t{itemIdentifier, 0, "noargs"},\n \t\ttPipe,\n-\t\t{itemIdentifier, "args"},\n-\t\t{itemNumber, "1"},\n-\t\t{itemString, `"hi"`},\n+\t\t{itemIdentifier, 0, "args"},\n+\t\t{itemNumber, 0, "1"},\n+\t\t{itemString, 0, `"hi"`},\n \t\ttRight,\n-\t\t{itemText, " outro"},\n+\t\t{itemText, 0, " outro"},\n \t\ttEOF,\n \t}},\n \t{"declaration", "{{$v := 3}}", []item{\n \t\ttLeft,\n-\t\t{itemVariable, "$v"},\n-\t\t{itemColonEquals, ":="},\n-\t\t{itemNumber, "3"},\n+\t\t{itemVariable, 0, "$v"},\n+\t\t{itemColonEquals, 0, ":="},\n+\t\t{itemNumber, 0, "3"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t{"2 declarations", "{{$v , $w := 3}}", []item{\n \t\ttLeft,\n-\t\t{itemVariable, "$v"},\n-\t\t{itemChar, ","},\n-\t\t{itemVariable, "$w"},\n-\t\t{itemColonEquals, ":="},\n-\t\t{itemNumber, "3"},\n+\t\t{itemVariable, 0, "$v"},\n+\t\t{itemChar, 0, ","},\n+\t\t{itemVariable, 0, "$w"},\n+\t\t{itemColonEquals, 0, ":="},\n+\t\t{itemNumber, 0, "3"},\n \t\ttRight,\n \t\ttEOF,\n \t}},\n \t// errors\n \t{"badchar", "#{{\\x01}}", []item{\n-\t\t{itemText, "#"},\n+\t\t{itemText, 0, "#"},\n \t\ttLeft,\n-\t\t{itemError, "unrecognized character in action: U+0001"},\n+\t\t{itemError, 0, "unrecognized character in action: U+0001"},\n \t}},\n \t{"unclosed action", "{{\\n}}", []item{\n \t\ttLeft,\n-\t\t{itemError, "unclosed action"},\n+\t\t{itemError, 0, "unclosed action"},\n \t}},\n \t{"EOF in action", "{{range", []item{\n \t\ttLeft,\n \t\ttRange,\n-\t\t{itemError, "unclosed action"},\n+\t\t{itemError, 0, "unclosed action"},\n \t}},\n \t{"unclosed quote", "{{\\"\\n\\"}}", []item{\n \t\ttLeft,\n-\t\t{itemError, "unterminated quoted string"},\n+\t\t{itemError, 0, "unterminated quoted string"},\n \t}},\n \t{"unclosed raw quote", "{{`xx\\n`}}", []item{\n \t\ttLeft,\n-\t\t{itemError, "unterminated raw quoted string"},\n+\t\t{itemError, 0, "unterminated raw quoted string"},\n \t}},\n \t{"unclosed char constant", "{{\\'\\n}}", []item{\n \t\ttLeft,\n-\t\t{itemError, "unterminated character constant"},\n+\t\t{itemError, 0, "unterminated character constant"},\n \t}},\n \t{"bad number", "{{3k}}", []item{\n \t\ttLeft,\n-\t\t{itemError, `bad number syntax: "3k"`},\n+\t\t{itemError, 0, `bad number syntax: "3k"`},\n \t}},\n \n \t// Fixed bugs\n@@ -213,10 +212,28 @@ func collect(t *lexTest, left, right string) (items []item) {\n \treturn\n }\n \n+func equal(i1, i2 []item, checkPos bool) bool {\n+\tif len(i1) != len(i2) {\n+\t\treturn false\n+\t}\n+\tfor k := range i1 {\n+\t\tif i1[k].typ != i2[k].typ {\n+\t\t\treturn false\n+\t\t}\n+\t\tif i1[k].val != i2[k].val {\n+\t\t\treturn false\t\t}\n+\t\tif checkPos && i1[k].pos != i2[k].pos {\n+\t\t\treturn false\n+\t\t}\n+\t}\n+\treturn true\n+}\n+\n func TestLex(t *testing.T) {\n \tfor _, test := range lexTests {\n \t\titems := collect(&test, "", "")\n-\t\tif !reflect.DeepEqual(items, test.items) {\n+\t\tif !equal(items, test.items, false) {\n \t\t\tt.Errorf("%s: got\\n\\t%v\\nexpected\\n\\t%v", test.name, items, test.items)\n \t\t}\n \t}\n@@ -226,13 +243,13 @@ func TestLex(t *testing.T) {\n var lexDelimTests = []lexTest{\n \t{"punctuation", "$$,@%{{}}@@", []item{\n \t\ttLeftDelim,\n-\t\t{itemChar, ","},\n-\t\t{itemChar, "@"},\n-\t\t{itemChar, "%"},\n-\t\t{itemChar, "{"},\n-\t\t{itemChar, "{"},\n-\t\t{itemChar, "}"},\n-\t\t{itemChar, "}"},\n+\t\t{itemChar, 0, ","},\n+\t\t{itemChar, 0, "@"},\n+\t\t{itemChar, 0, "%"},\n+\t\t{itemChar, 0, "{"},\n+\t\t{itemChar, 0, "{"},\n+\t\t{itemChar, 0, "}"},\n+\t\t{itemChar, 0, "}"},\n \t\ttRightDelim,\n \t\ttEOF,\n \t}},\n@@ -243,15 +260,57 @@ var lexDelimTests = []lexTest{\n }\n \n var (\n-\ttLeftDelim = item{itemLeftDelim, "$$"}\n-\ttRightDelim = item{itemRightDelim, "@@"}\n+\ttLeftDelim = item{itemLeftDelim, 0, "$$"}\n+\ttRightDelim = item{itemRightDelim, 0, "@@"}\n )\n \n func TestDelims(t *testing.T) {\n \tfor _, test := range lexDelimTests {\n \t\titems := collect(&test, "$$", "@@")\n-\t\tif !reflect.DeepEqual(items, test.items) {\n+\t\tif !equal(items, test.items, false) {\n+\t\t\tt.Errorf("%s: got\\n\\t%v\\nexpected\\n\\t%v", test.name, items, test.items)\n+\t\t}\n+\t}\n+}\n+\n+var lexPosTests = []lexTest{\n+\t{\"empty\", "", []item{tEOF}},\n+\t{\"punctuation\", "{{,@%#}}", []item{\n+\t\t{itemLeftDelim, 0, "{{"},\n+\t\t{itemChar, 2, ","},\n+\t\t{itemChar, 3, "@"},\n+\t\t{itemChar, 4, "%"},\n+\t\t{itemChar, 5, "#"},\n+\t\t{itemRightDelim, 6, "}}""},\n+\t\t{itemEOF, 8, ""},\n+\t}},\n+\t{\"sample\", "0123{{hello}}xyz", []item{\n+\t\t{itemText, 0, "0123"},\n+\t\t{itemLeftDelim, 4, "{{"},\n+\t\t{itemIdentifier, 6, "hello"},\n+\t\t{itemRightDelim, 11, "}}""},\n+\t\t{itemText, 13, "xyz"},\n+\t\t{itemEOF, 16, ""},\n+\t}},\n+}\n+\n+// The other tests don't check position, to make the test cases easier to construct.\n+// This one does.\n+func TestPos(t *testing.T) {\n+\tfor _, test := range lexPosTests {\n+\t\titems := collect(&test, "", "")\n+\t\tif !equal(items, test.items, true) {\n \t\t\tt.Errorf("%s: got\\n\\t%v\\nexpected\\n\\t%v", test.name, items, test.items)\n+\t\t\tif len(items) == len(test.items) {\n+\t\t\t\t// Detailed print; avoid item.String() to expose the position value.\n+\t\t\t\tfor i := range items {\n+\t\t\t\t\tif !equal(items[i:i+1], test.items[i:i+1], true) {\n+\t\t\t\t\t\ti1 := items[i]\n+\t\t\t\t\t\ti2 := test.items[i]\n+\t\t\t\t\t\tt.Errorf("\\t#%d: got {%v %d %q} expected {%v %d %q}", i, i1.typ, i1.pos, i1.val, i2.typ, i2.pos, i2.val)\n+\t\t\t\t\t}\n+\t\t\t\t}\n+\t\t\t}\n \t\t}\n \t}\n }\n```
## コアとなるコードの解説
### `src/pkg/text/template/parse/lex.go` の変更点
1. **`item` 構造体の変更**:
- `pos int` フィールドが追加されました。これは、字句解析されたトークンが入力文字列のどのバイトオフセットから始まるかを示すものです。これにより、各トークンが自身の位置情報を持つようになり、後で行番号計算の基準として利用されます。
2. **`lexer` 構造体の変更**:
- `lastPos int` フィールドが追加されました。このフィールドは、`nextItem` メソッドが最後にクライアントに返した `item` の開始位置(`pos`)を記録するために使用されます。これにより、`lineNumber` の計算が、`lexer` の現在の進行中の位置ではなく、確定したトークンの位置に基づいて行われるようになります。
3. **`emit` メソッドの変更**:
- `l.items <- item{t, l.input[l.start:l.pos]}` が `l.items <- item{t, l.start, l.input[l.start:l.pos]}` に変更されました。これは、`item` を生成してチャンネルに送信する際に、新しく追加された `pos` フィールドに `l.start` (現在のトークンの開始位置) を明示的に設定するようにしたものです。
4. **`lineNumber` メソッドの変更**:
- `return 1 + strings.Count(l.input[:l.pos], "\\n")` が `return 1 + strings.Count(l.input[:l.lastPos], "\\n")` に変更されました。この変更がデータ競合の修正の核心です。行番号の計算に `l.pos` (字句解析器の現在の位置) ではなく、`l.lastPos` (最後に返されたトークンの位置) を使用することで、`lineNumber` の計算が、字句解析器の進行中の状態に依存しなくなり、データ競合が解消されます。
5. **`errorf` メソッドの変更**:
- `l.items <- item{itemError, fmt.Sprintf(format, args...)}` が `l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)}` に変更されました。`emit` と同様に、エラーを表す `item` を生成する際にも、そのエラーが発生した位置 (`l.start`) を `pos` フィールドに設定するようにしました。
6. **`nextItem` メソッドの変更**:
- `item := <-l.items` の後に `l.lastPos = item.pos` が追加されました。`nextItem` は `l.items` チャンネルから次のトークンを受け取りますが、この変更により、受け取ったトークンの開始位置 (`item.pos`) を `l.lastPos` に保存するようになりました。これにより、`lineNumber` メソッドが常に最新の確定したトークンの位置を参照できるようになります。
### `src/pkg/text/template/parse/lex_test.go` の変更点
1. **`reflect` パッケージの削除**:
- `import ("reflect")` が削除されました。これは、`reflect.DeepEqual` の代わりにカスタムの `equal` 関数を使用するようになったためです。
2. **`lexTest` の `item` 初期化の変更**:
- `item` 構造体に `pos` フィールドが追加されたため、既存のすべての `item` の初期化において、`typ` と `val` の間に `0` が追加されました。これは、テストの目的上、既存のテストケースでは `pos` の値が重要ではないため、デフォルト値の `0` を設定しています。
3. **`equal` 関数の追加**:
- `func equal(i1, i2 []item, checkPos bool) bool` という新しいヘルパー関数が追加されました。この関数は、2つの `item` スライスを比較し、`checkPos` が `true` の場合にのみ `pos` フィールドも比較します。これにより、既存のテストは `pos` を無視して実行でき、新しい `TestPos` 関数では `pos` を厳密にチェックできるようになります。
4. **`TestLex` および `TestDelims` の変更**:
- `!reflect.DeepEqual(items, test.items)` が `!equal(items, test.items, false)` に変更されました。これにより、既存の字句解析テストは、`pos` フィールドの比較を行わずに実行されます。
5. **`lexPosTests` 変数の追加**:
- `lexPosTests` という新しい `lexTest` スライスが追加されました。このテストケースは、`item` の `pos` フィールドが正しく設定されていることを検証するために特別に設計されています。
6. **`TestPos` 関数の追加**:
- `func TestPos(t *testing.T)` という新しいテスト関数が追加されました。この関数は `lexPosTests` を使用し、`equal` 関数を `checkPos` を `true` にして呼び出すことで、`item` の `pos` フィールドが期待通りに設定されていることを厳密に検証します。これにより、データ競合修正の副作用として導入された `pos` フィールドの正確性が保証されます。
## 関連リンク
* GitHubコミットページ: [https://github.com/golang/go/commit/20d9fd3ae18bd5f80c3c0f8f424ebd9a72b6788a](https://github.com/golang/go/commit/20d9fd3ae18bd5f80c3c0f8f424ebd9a72b6788a)
* Go CL (Code Review): [https://golang.org/cl/6445061](https://golang.org/cl/6445061)
* Go Issue #3886: (直接のリンクは見つかりませんでしたが、コミットメッセージで参照されています)
## 参考にした情報源リンク
* Go CL 6445061: [https://golang.org/cl/6445061](https://golang.org/cl/6445061)
* データ競合に関する一般的な情報 (Goのドキュメントなど):
* The Go Programming Language Specification - Memory Model: [https://go.dev/ref/mem](https://go.dev/ref/mem)
* Go Race Detector: [https://go.dev/blog/race-detector](https://go.dev/blog/race-detector)
* 字句解析器に関する一般的な情報 (コンパイラの教科書など)