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

[インデックス 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 は、このエラーが発生した行番号を計算するために使用されます。

itemitemType

text/template/parse パッケージでは、字句解析器が生成するトークンを item 構造体で表現します。itemType は、そのトークンの種類(例: itemTextitemLeftDelimitemNumber など)を定義する列挙型です。

技術的詳細

このデータ競合は、lexer 構造体の lineNumber() メソッドが l.pos (現在の入力文字列における字句解析器の位置) を直接使用して行番号を計算していたために発生しました。l.poslexer の内部状態であり、nextItem() メソッドが l.items チャンネルからアイテムを読み取る際に、l.pos が更新される可能性があります。複数のゴルーチンが nextItem() を呼び出すような状況では、lineNumber()l.pos を読み取るタイミングと、別のゴルーチンが l.pos を書き込むタイミングが競合し、不正な行番号が計算される可能性がありました。

修正の核心は、lineNumber の計算を lexer の現在の状態に依存するのではなく、nextItem によって返された最後の item の位置に依存するように変更することです。これにより、lineNumber の計算が、すでに確定したトークンの位置に基づいて行われるため、データ競合が解消されます。

具体的には、以下の変更が行われました。

  1. 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.
    }
    
  2. lexer 構造体への lastPos フィールドの追加: lexer 構造体に lastPos int フィールドが追加されました。これは、nextItem メソッドが最後に返した item の開始位置を保持します。

    type lexer struct {
    	// ...
    	lastPos    int       // position of nost recent item returned by nextItem
    	// ...
    }
    
  3. emit メソッドの変更: emit メソッドは、iteml.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
    }
    
  4. errorf メソッドの変更: errorf メソッドも同様に、エラーアイテムを生成する際に l.startpos として渡すように変更されました。

    func (l *lexer) errorf(format string, args ...interface{}) stateFn {
    	l.items <- item{itemError, l.start, fmt.Sprintf(format, args...)} // l.start を pos として渡す
    	return nil
    }
    
  5. nextItem メソッドの変更: nextItem メソッドは、l.items チャンネルから item を受け取った後、その itemposl.lastPos に保存するように変更されました。これにより、lineNumber が常に最後に処理されたトークンの位置に基づいて計算されるようになります。

    func (l *lexer) nextItem() item {
    	item := <-l.items
    	l.lastPos = item.pos // ここで lastPos を更新
    	return item
    }
    
  6. 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 の初期化に 0pos の値として追加する変更が行われました。さらに、equal 関数が導入され、reflect.DeepEqual の代わりに item の比較を行うようになりました。これは、pos フィールドの比較をオプションにするためです。そして、TestPos という新しいテスト関数が追加され、itempos フィールドが正しく設定されていることを明示的に検証するようになりました。

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

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)
*   字句解析器に関する一般的な情報 (コンパイラの教科書など)