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

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

このコミットは、Go言語の標準ライブラリである text/template/parse パッケージにおける字句解析器(lexer)の挙動に関する変更です。具体的には、閉じデリミタ(}} など)の不一致を検出するエラー処理を、以前の状態に「ほぼ巻き戻す」ものです。これは、特にミニファイされたJavaScriptを含むテンプレートとの互換性を維持するために行われました。

コミット

commit c842e43ef6a7e3ac525a0a72e91dc7c482857afd
Author: Rob Pike <r@golang.org>
Date:   Fri Sep 13 12:44:45 2013 +1000

    text/template/parse: mostly roll back the error detection for unmatched right delimiters
    It's too late to change this behavior: it breaks templates with minimized JavaScript.
    
    Makes me sad because this common error can never be caught: "{foo}}".
    Three cheers for compatibility.
    
    (Leave in a fix to a broken test.)
    
    R=golang-dev, dsymonds, rsc
    CC=golang-dev
    https://golang.org/cl/13689043

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

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

元コミット内容

このコミットは、text/template/parse パッケージにおいて、不一致の閉じデリミタ(例えば {{foo}}} のような、閉じデリミタが余分にあるケース)を検出するエラー処理を、以前の挙動に「ほぼ巻き戻す」ものです。以前の変更で導入された、左デリミタの直後に閉じデリミタが続く場合にエラーを発生させるロジックが削除されました。

コミットメッセージによると、この変更は「ミニファイされたJavaScriptを含むテンプレートを壊してしまうため、この挙動を変更するには手遅れである」とされています。これにより、{{foo}}} のような一般的な誤り(閉じデリミタが一つ多い)を捕捉できなくなることへの懸念が表明されていますが、互換性を優先した結果であると述べられています。また、関連するテストの修正も含まれています。

変更の背景

この変更の背景には、Go言語の text/template パッケージが、特にウェブアプリケーションのフロントエンドで広く利用される中で直面した現実的な問題があります。

  1. ミニファイされたJavaScriptとの衝突: JavaScriptコード、特に本番環境で利用されるためにサイズを削減(ミニファイ)されたJavaScriptコードは、しばしば }} のような文字列を含みます。例えば、オブジェクトリテラルの終了やアロー関数のブロック終了など、JavaScriptの構文として正当な }} のシーケンスが頻繁に現れます。
  2. テンプレートエンジンのデリミタ: Goの text/template パッケージは、デフォルトで {{}} をアクションの開始と終了を示すデリミタとして使用します。
  3. 以前の変更による問題: このコミット以前に、text/template/parse の字句解析器は、左デリミタの直後に閉じデリミタが続く場合(または、テキスト中に閉じデリミタが予期せず現れた場合)に、それを「不一致の閉じデリミタ」としてエラーを発生させるように変更されていました。この変更は、テンプレートの記述ミスを早期に検出することを意図していたと考えられます。
  4. 互換性の破壊: しかし、この厳格なエラー検出は、ミニファイされたJavaScriptを埋め込んだ既存のGoテンプレートを壊してしまうという副作用をもたらしました。JavaScript内の }} が、テンプレートの閉じデリミタとして誤って解釈され、構文エラーとして報告されてしまったのです。
  5. Go 1.1リリース後の対応: コミットメッセージに「Go 1.1より前に修正すべきだった」とあることから、この問題はGo 1.1のリリース(2013年5月)後に顕在化し、既存のアプリケーションに影響を与えていることが示唆されます。Go言語は後方互換性を非常に重視するため、このような破壊的な変更は許容されません。
  6. 互換性優先の決断: そのため、たとえ {{foo}}} のような一般的なテンプレート記述ミスを捕捉できなくなるという「悲しい」結果になったとしても、既存のテンプレートが動作し続けることを保証するために、エラー検出ロジックを巻き戻すという決断が下されました。これは、言語やライブラリの安定性と実用性を優先するGoの設計哲学を強く反映しています。

前提知識の解説

このコミットを理解するためには、以下の概念を把握しておく必要があります。

  1. Go言語の text/template パッケージ:

    • Go言語に組み込まれているテキストテンプレートエンジンです。HTML、XML、プレーンテキストなど、任意のテキスト形式の出力を生成するために使用されます。
    • テンプレートは、プレーンテキストと「アクション」と呼ばれる特殊な構文の組み合わせで構成されます。アクションは、データ構造の値の表示、条件分岐、ループなどを制御します。
    • デフォルトのデリミタは {{}} です。例えば、Hello, {{.Name}}! は、.Name の値に置き換えられます。
  2. 字句解析(Lexing/Tokenizing):

    • コンパイラやインタプリタの最初の段階です。入力された文字列(この場合はテンプレートのソースコード)を、意味のある最小単位である「トークン」(または「字句」)のストリームに分解するプロセスです。
    • 例えば、{{.Name}} という文字列は、{{(左デリミタ)、.Name(識別子)、}}(右デリミタ)といったトークンに分解されます。
    • text/template/parse パッケージは、この字句解析を担当し、テンプレート文字列を解析ツリー(AST)に変換する前段階のトークン化を行います。
  3. 有限状態オートマトン(Finite State Automaton, FSA)と字句解析器(Lexer):

    • 多くの字句解析器は、有限状態オートマトンに基づいて実装されます。これは、現在の状態と入力文字に基づいて次の状態と出力トークンを決定するモデルです。
    • text/template/parse/lex.go 内の stateFn 型の関数は、この状態遷移関数を表しています。lexText はテキストモードでの字句解析を担当する状態関数です。
  4. デリミタの役割:

    • テンプレートエンジンにおいて、デリミタはプレーンテキストとテンプレートのアクションを区別するための境界線です。
    • text/template では、{{ がアクションの開始、}} がアクションの終了を示します。
  5. ミニファイされたJavaScript:

    • ウェブアプリケーションのパフォーマンス最適化のために、JavaScriptコードから不要な空白、改行、コメントなどを除去し、変数名を短縮するなどしてファイルサイズを最小化したものです。
    • ミニファイされたコードは、人間が読むには非常に難解ですが、ブラウザによる解析・実行には問題ありません。
    • ミニファイの過程で、}} のような特定の文字シーケンスが、元のコードでは離れていた場所で隣接するようになることがあります。例えば、if (x) { return { a: 1 }; } がミニファイされると if(x){return{a:1}} のようになり、}} が連続して現れることがあります。
  6. 後方互換性(Backward Compatibility):

    • ソフトウェア開発において、新しいバージョンのソフトウェアが古いバージョンのデータ、ファイル、またはコードと互換性があることを指します。
    • Go言語は、特にメジャーバージョン内(例: Go 1.x)での後方互換性を非常に重視しており、既存のコードが新しいバージョンで動作しなくなるような変更は極力避ける方針を取っています。このコミットは、この原則を強く反映したものです。

技術的詳細

このコミットの技術的詳細は、text/template/parse パッケージの字句解析器(lexer)がどのように動作し、なぜ特定の }} シーケンスの検出を巻き戻す必要があったのかに集約されます。

text/template/parse/lex.go は、テンプレート文字列をトークンに分割する役割を担っています。このファイルには、字句解析の状態を管理する lexer 構造体と、各状態に対応する stateFn 型の関数群が含まれています。

変更前のコードでは、lexText 関数(テキストモードで字句解析を行う状態関数)内に、以下のようなロジックが存在していました。

		// Check for right after left in case they're the same.
		if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
			return l.errorf("unmatched right delimiter")
		}

このコードは、現在の字句解析器の読み取り位置 l.pos から始まる入力文字列 l.input が、テンプレートの閉じデリミタ l.rightDelim(デフォルトでは }})で始まっているかどうかを strings.HasPrefix を使ってチェックしていました。

変更前のロジックの意図: このチェックの意図は、おそらく以下のような誤ったテンプレート構文を早期に検出することでした。

  • {{.Var}}}: アクションの閉じデリミタが一つ多い。
  • Hello }}: プレーンテキスト中に予期せず閉じデリミタが現れた。 特に {{.Var}}} のようなケースでは、{{ の後に . が続き、その後に Var が来て、そして }} が来るはずですが、その直後にさらに } が続く場合、それはテンプレートの構文としては不正です。このロジックは、このような「不一致の閉じデリミタ」をエラーとして報告しようとしていました。

変更による影響: しかし、この厳格なチェックは、ミニファイされたJavaScriptコードを含むHTMLテンプレートなどで問題を引き起こしました。ミニファイされたJavaScriptでは、例えば以下のようなコードが生成されることがあります。

// 元のコード
var obj = {
    key: value
};
if (condition) {
    // ...
}

これがミニファイされると、var obj={key:value};if(condition){...}} のように、JavaScriptの構文として正当な }} の連続が出現することがあります。

変更前の lex.go のロジックは、このようなJavaScript内の }} を、テンプレートの「不一致の閉じデリミタ」と誤解釈し、テンプレートのパースエラーを発生させていました。これは、Goのテンプレートエンジンが、JavaScriptのような他の言語のコードを埋め込むことを想定している場合に、深刻な後方互換性の問題を引き起こしました。

コミットによる修正: このコミットでは、上記の strings.HasPrefix を用いたエラー検出ロジックが完全に削除されました。これにより、字句解析器は、左デリミタの直後に閉じデリミタが続く場合でも、それをエラーとして扱わず、単なるプレーンテキストの一部として処理するようになります。

結果として、{{foo}}} のようなテンプレートは、{{foo}} がアクションとして解釈された後、残りの } は単なるテキストとして扱われることになります。これは、テンプレートの記述ミスを捕捉できなくなるという欠点がありますが、ミニファイされたJavaScriptを含む既存のテンプレートが正しく動作するという互換性を優先したトレードオフです。

lex_test.go の変更は、この新しい(巻き戻された)挙動を反映しています。以前は unmatched right delimiter エラーを期待していたテストケース {"unmatched right delimiter", "hello-{.}}-world", ...} が、エラーではなく hello-{.}}-world というテキスト全体を itemText として解釈するように変更されました。これは、字句解析器が }} を特別なデリミタとしてではなく、通常のテキストの一部として扱うようになったことを示しています。

この変更は、Go言語の標準ライブラリが、理論的な厳密さよりも実用性と後方互換性を重視する設計哲学を明確に示しています。

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

このコミットによるコードの変更は、主に2つのファイルにわたります。

  1. src/pkg/text/template/parse/lex.go: 字句解析器の本体

    • 以下の4行が削除されました。
    --- a/src/pkg/text/template/parse/lex.go
    +++ b/src/pkg/text/template/parse/lex.go
    @@ -217,10 +217,6 @@ func lexText(l *lexer) stateFn {
     		// Check for right after left in case they're the same.
     		if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
     			return l.errorf("unmatched right delimiter")
     		}
     		if l.next() == eof {
     			break
     		}
    
  2. src/pkg/text/template/parse/lex_test.go: 字句解析器のテストファイル

    • 既存のテストケースが1つ修正されました。
    --- a/src/pkg/text/template/parse/lex_test.go
    +++ b/src/pkg/text/template/parse/lex_test.go
    @@ -340,8 +340,11 @@ var lexTests = []lexTest{
     		{itemText, 0, "hello-"},
     		{itemError, 0, `comment ends before closing delimiter`},
     	}},
    +	// This one is an error that we can't catch because it breaks templates with
    +	// minimized JavaScript. Should have fixed it before Go 1.1.
     	{"unmatched right delimiter", "hello-{.}}-world", []item{
    -		{itemError, 0, `unmatched right delimiter`},
    +		{itemText, 0, "hello-{.}}-world"},
    +		tEOF,
     	}},
     }
    

コアとなるコードの解説

src/pkg/text/template/parse/lex.go の変更

削除されたコードは lexText 関数内にありました。lexText は、テンプレートのプレーンテキスト部分を解析する際の字句解析器の状態関数です。この関数は、入力ストリームを読み進め、テンプレートのアクションデリミタ({{)やその他の特殊文字を検出する役割を担っています。

削除されたブロックは以下の通りです。

		// Check for right after left in case they're the same.
		if strings.HasPrefix(l.input[l.pos:], l.rightDelim) {
			return l.errorf("unmatched right delimiter")
		}
  • l.input[l.pos:]: これは、字句解析器の現在の読み取り位置 l.pos から入力文字列の最後までをスライスしたものです。つまり、これから読み取るべき残りの文字列を表します。
  • l.rightDelim: これは、テンプレートの閉じデリミタ(デフォルトでは }})です。
  • strings.HasPrefix(l.input[l.pos:], l.rightDelim): この条件式は、「これから読み取る文字列が、閉じデリミタで始まっているか?」をチェックしています。
  • return l.errorf("unmatched right delimiter"): もし上記の条件が真であれば、字句解析器は「不一致の閉じデリミタ」というエラーを発生させていました。

このロジックが削除されたことにより、字句解析器は、入力ストリーム中に閉じデリミタ }} が現れても、それがテンプレートのアクションの開始デリミタ {{ の直後でなければ、単なるプレーンテキストの一部として扱います。これにより、ミニファイされたJavaScript内の }} が誤ってエラーとして検出されることがなくなりました。

src/pkg/text/template/parse/lex_test.go の変更

テストファイルの変更は、lex.go の変更によって字句解析器の挙動が変わったことを反映しています。

変更前のテストケースは以下の通りでした。

	{"unmatched right delimiter", "hello-{.}}-world", []item{
		{itemError, 0, `unmatched right delimiter`},
	}},

このテストは、入力文字列 "hello-{.}}-world" に対して、unmatched right delimiter というエラーが itemError として返されることを期待していました。これは、{.}} の部分で }} が予期せず現れた場合にエラーを検出するという、以前の lex.go のロジックに基づいています。

変更後のテストケースは以下の通りです。

	// This one is an error that we can't catch because it breaks templates with
	// minimized JavaScript. Should have fixed it before Go 1.1.
	{"unmatched right delimiter", "hello-{.}}-world", []item{
		{itemText, 0, "hello-{.}}-world"},
		tEOF,
	}},
  • 新しいコメントが追加され、このケースがミニファイされたJavaScriptとの互換性のために捕捉できないエラーであることが明記されています。
  • 期待される結果が itemError から itemText に変更されました。これは、入力文字列 "hello-{.}}-world" 全体が、エラーではなく単一のプレーンテキスト itemText として解釈されるようになったことを意味します。
  • tEOF は、入力の終端(End Of File)を示すトークンです。

このテストの変更は、lex.go からエラー検出ロジックが削除された結果、字句解析器が }} を含む文字列をエラーとして扱わず、単なるテキストとして通過させるようになったことを正確に示しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴
  • Go言語のテンプレートに関する一般的な知識
  • 字句解析とコンパイラの基本原理に関する知識
  • JavaScriptのミニファイに関する一般的な知識