[インデックス 17409] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージにおいて、テンプレートの if-else
構文に else if
のサポートを追加するものです。具体的には、パーサーが {{if A}}a{{else if B}}b{{end}}
のような記述を、内部的に {{if A}}a{{else}}{{if B}}b{{end}}{{end}}
のようにネストされた if-else
構造として解釈するように変更されています。これにより、テンプレートの記述がより簡潔になり、可読性が向上します。
コミット
- コミットハッシュ:
37cee77ac681654a20939faa047f11b308908902
- Author: Rob Pike r@golang.org
- Date: Wed Aug 28 14:43:56 2013 +1000
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/37cee77ac681654a20939faa047f11b308908902
元コミット内容
text/template: allow {{else if ... }} to simplify if chains
The method is simple: the parser just parses
{{if A}}a{{else if B}}b{{end}}
to the same tree that would be produced by
{{if A}}a{{else}}{{if B}}b{{end}}{{end}}
Thus no changes are required in text/template itself
or in html/template, only in text/template/parse.
Fixes #6085
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/13327043
変更の背景
この変更の背景には、Goの text/template
パッケージにおける if-else
構文の冗長性がありました。従来の text/template
では、複数の条件を連鎖させる場合、以下のようにネストされた if-else
を記述する必要がありました。
{{if condition1}}
// do something
{{else}}
{{if condition2}}
// do something else
{{else}}
{{if condition3}}
// do yet another thing
{{end}}
{{end}}
{{end}}
このような記述は、条件が増えるにつれてインデントが深くなり、テンプレートの可読性と保守性を著しく低下させます。他の多くのプログラミング言語やテンプレートエンジンでは else if
構文が一般的にサポートされており、これにより条件の連鎖をよりフラットかつ簡潔に記述できます。
このコミットは、この問題を解決し、ユーザーがより自然で読みやすい else if
構文を使用できるようにすることを目的としています。コミットメッセージにある Fixes #6085
は、この機能追加が特定の課題(おそらくGitHubのIssueトラッカーで報告されたもの)に対応していることを示唆しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と text/template
パッケージの内部動作に関する知識が必要です。
-
Go言語の
text/template
パッケージ:- Go言語に組み込まれているテキストテンプレートエンジンです。
- データ構造をテキスト出力に変換するために使用されます。
{{...}}
で囲まれたアクション(制御構造、データアクセスなど)と、それ以外のリテラルテキストで構成されます。- 主な制御構造には
if
、range
、with
などがあります。
-
テンプレートのパース(Parsing):
- テンプレートエンジンは、まず与えられたテンプレート文字列を解析(パース)し、内部的な表現(通常は抽象構文木、AST)に変換します。
- このASTは、テンプレートの構造とロジックを表現するノードのツリーです。
text/template
パッケージでは、text/template/parse
サブパッケージがこのパース処理を担当します。
-
抽象構文木(Abstract Syntax Tree, AST):
- ソースコードやテンプレートの構造を木構造で表現したものです。
- 各ノードは、プログラムの構成要素(変数、演算子、制御構造など)に対応します。
- テンプレートエンジンは、このASTをトラバースして最終的な出力を生成します。
-
if
/else
制御構造:text/template
におけるif
アクションは、パイプライン(式)の評価結果に基づいて条件分岐を行います。{{if pipeline}} T1 {{else}} T0 {{end}}
の形式で記述され、pipeline
が真と評価されればT1
が実行され、偽と評価されればT0
が実行されます。
-
字句解析(Lexing)と構文解析(Parsing):
- 字句解析(Lexing): テンプレート文字列をトークン(意味を持つ最小単位、例:
if
キーワード、else
キーワード、識別子、区切り文字など)のストリームに分解するプロセス。text/template/parse
パッケージではitem
型がトークンを表します。 - 構文解析(Parsing): トークンのストリームを文法規則に従って解析し、ASTを構築するプロセス。このコミットの変更は、主にこの構文解析の段階で行われます。
- 字句解析(Lexing): テンプレート文字列をトークン(意味を持つ最小単位、例:
このコミットの核心は、else if
という新しい構文を導入するのではなく、既存のパーサーが else if
を検出した際に、それをネストされた if-else
構造としてASTに変換するという点にあります。これにより、text/template
や html/template
の実行エンジン自体には変更を加える必要がなく、パーサーのみの変更で機能を実現しています。
技術的詳細
このコミットの技術的詳細は、text/template/parse
パッケージにおける構文解析ロジックの変更に集約されます。
従来の text/template
のパーサーは、{{else}}
の後に続く内容を、現在の if
ブロックの代替(else節)としてのみ解釈していました。しかし、この変更では、{{else}}
の直後に if
キーワードが続く場合(すなわち {{else if ...}}
の形式)、パーサーはこれを特別なケースとして扱います。
具体的には、パーサーは {{else if ...}}
を検出すると、以下のように動作します。
else
トークンを処理します。- 次に
if
トークンが続くことを検出します。 - この
if
を、現在のelse
節の内部に新しいif
ブロックとしてパースします。 - この新しい
if
ブロックが終了する{{end}}
までをパースし、その後の{{end}}
は、外側のif
ブロックの終了として扱われます。
これにより、{{if A}}a{{else if B}}b{{end}}
というテンプレート文字列は、パーサーによって以下のようなASTに変換されます。
IfNode (A)
- ListNode (a)
- ElseNode
- IfNode (B)
- ListNode (b)
- (no else)
このASTは、手動で {{if A}}a{{else}}{{if B}}b{{end}}{{end}}
と記述した場合に生成されるASTと全く同じになります。したがって、text/template
や html/template
の実行エンジンは、この新しい構文を特別に意識することなく、既存のロジックで正しくテンプレートを実行できます。
このアプローチの利点は以下の通りです。
- 後方互換性: 既存のテンプレートや実行エンジンに影響を与えません。
- コードの簡潔さ: テンプレートユーザーは
else if
を使用してコードを簡潔に記述できます。 - 実装の単純さ: 変更がパーサー層に限定されるため、実装が比較的単純です。
コミットメッセージには「TODO: Should we allow else-if in with and range?」というコメントがあり、これは将来的に with
や range
アクションでも同様の else if
構文をサポートする可能性を示唆しています。しかし、このコミットでは if
アクションにのみ適用されています。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、src/pkg/text/template/parse/parse.go
ファイルに集中しています。
-
src/pkg/text/template/parse/parse.go
:parseControl
関数のシグネチャが変更され、allowElseIf bool
という新しい引数が追加されました。この引数は、現在の制御構造がelse if
を許可するかどうかを制御します。parseControl
関数内で、nodeElse
のケースにallowElseIf
がtrue
の場合にelse if
を処理するロジックが追加されました。具体的には、t.peek().typ == itemIf
でelse
の直後にif
が続くかをチェックし、続く場合は新しいif
ノードをelseList
に追加します。ifControl
関数がnewIf(t.parseControl(true, "if"))
を呼び出すように変更され、if
アクションがelse if
を許可するように設定されました。rangeControl
とwithControl
関数はnewRange(t.parseControl(false, "range"))
およびnewWith(t.parseControl(false, "with"))
を呼び出すように変更され、これらがelse if
を許可しないように設定されました。elseControl
関数に、{{else if ...}}
のケースを特別に処理するためのロジックが追加されました。t.peekNonSpace().typ == itemIf
でelse
の直後にif
が続くかをチェックし、その場合はnewElse
を返すことで、パーサーがelse
の後に続くif
を適切に処理できるようにします。
-
src/pkg/text/template/doc.go
:if
アクションのドキュメントに、{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
の新しい構文とその解釈に関する説明が追加されました。
-
src/pkg/text/template/exec_test.go
:if else if
とif else chain
の新しいテストケースが追加され、else if
構文が正しく機能することを確認しています。
-
src/pkg/text/template/parse/parse_test.go
:if with else if
とif else chain
の新しいパーステストケースが追加され、else if
構文が正しくASTに変換されることを確認しています。extra end after if
というエラーケースのテストも追加され、{{if .X}}a{{else if .Y}}b{{end}}{{end}}
のように余分な{{end}}
がある場合にエラーが報告されることを確認しています。
コアとなるコードの解説
src/pkg/text/template/parse/parse.go
の変更点
最も重要な変更は parseControl
関数と elseControl
関数にあります。
parseControl
関数の変更
// func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { // Old signature
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) { // New signature
// ...
switch next.Type() {
case nodeEnd: //done
case nodeElse:
if allowElseIf { // 新しい allowElseIf フラグ
// Special case for "else if". If the "else" is followed immediately by an "if",
// the elseControl will have left the "if" token pending. Treat
// {{if a}}_{{else if b}}_{{end}}
// as
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
// is assumed. This technique works even for long if-else-if chains.
// TODO: Should we allow else-if in with and range?
if t.peek().typ == itemIf { // else の直後に if が続くかチェック
t.next() // Consume the "if" token.
elseList = newList(next.Position())
elseList.append(t.ifControl()) // 新しい if ブロックを elseList に追加
// Do not consume the next item - only one {{end}} required.
break
}
}
elseList, next = t.itemList()
// ...
}
// ...
}
parseControl
は、if
,range
,with
のような制御構造をパースする汎用関数です。allowElseIf
引数が追加され、現在の制御構造がelse if
を許可するかどうかを明示的に指定できるようになりました。if
アクションの場合はtrue
、range
やwith
の場合はfalse
が渡されます。nodeElse
のケースで、allowElseIf
がtrue
かつelse
の直後にif
トークン (itemIf
) が続く場合、パーサーはt.next()
でif
トークンを消費し、t.ifControl()
を呼び出して新しいif
ブロックをパースします。この新しいif
ブロックは、現在のelse
節のコンテンツとしてelseList
に追加されます。- このロジックにより、
{{else if ...}}
は内部的に{{else}}{{if ...}}{{end}}
と同じAST構造に変換されます。
elseControl
関数の変更
func (t *Tree) elseControl() Node {
// Special case for "else if".
peek := t.peekNonSpace()
if peek.typ == itemIf { // else の直後に if が続くかチェック
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
return newElse(peek.pos, t.lex.lineNumber())
}
return newElse(t.expect(itemRightDelim, "else").pos, t.lex.lineNumber())
}
elseControl
は{{else}}
アクションをパースする関数です。t.peekNonSpace()
を使用して、else
の直後に空白以外のトークンが何かを調べます。- もしそれが
itemIf
であれば、これは{{else if ...}}
のパターンであることを意味します。この場合、newElse
を返すことで、パーサーはelse
ノードを生成し、その後のif
はparseControl
関数内のロジックによって適切に処理されるようになります。これにより、else if
が単一のelse
ノードとして扱われるのではなく、else
の後に続くif
として解釈される道筋が作られます。
src/pkg/text/template/doc.go
の変更点
ドキュメントの追加は、この新機能の利用方法をユーザーに伝える上で非常に重要です。
{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}
To simplify the appearance of if-else chains, the else action
of an if may include another if directly; the effect is exactly
the same as writing
{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
この説明は、else if
が単なる糖衣構文(syntactic sugar)であり、内部的にはネストされた if
と同じであることを明確に示しています。
テストケースの追加
exec_test.go
と parse_test.go
に追加されたテストケースは、この機能が期待通りに動作することを確認します。特に if else chain
のテストは、複数の else if
が連鎖する場合の正しい動作を検証しています。
// src/pkg/text/template/exec_test.go
{"if else if", "{{if false}}FALSE{{else if true}}TRUE{{end}}", "TRUE", tVal, true},
{"if else chain", "{{if eq 1 3}}1{{else if eq 2 3}}2{{else if eq 3 3}}3{{end}}", "3", tVal, true},
// src/pkg/text/template/parse/parse_test.go
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+\", noError,
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
これらのテストは、else if
が正しく評価され、またパース時に期待されるAST構造に変換されることを保証します。
関連リンク
- Go CL 13327043: https://golang.org/cl/13327043
参考にした情報源リンク
- コミットメッセージ:
37cee77ac681654a20939faa047f11b308908902
- Go言語
text/template
パッケージのドキュメント - Go言語
text/template/parse
パッケージのソースコード