年末なのでtext/template周りを歩いて回ってみた
Buenos Dias.
Pairs事業部でエンジニアをやっている @MasashiSalvadorです。
業務ではGo言語を使ったバックエンドの実装、フロントエンドの実装、もしくは片手間でPythonでデータを弄るようなこともやっています。
今回のブログはeureka Advent Calendar 2016の18日目の記事です。17日目は大久保さんのデータ分析の誤りを未然に防ぐ! SQL4つの検算テクニック | eureka tech blogでした!
今回の記事ではGo言語の text/template
の基本的な使い方を始めに整理し、次に text/template
のコードを追って得た知見について少しお話ししたいと思います。
記事を書くに至った経緯
僕の担当するチームでは、主にユーザに配信するメールでテンプレートエンジンを利用することが多いです。
Go言語の text/template
は薄くてシンプルであるという特徴を持っています。
機能の数は多くはないものの、一般論として「テンプレートエンジン」を用いて実現したいことは大体実現できます。
しかし、
- 使ってみないとわからないハマりどころが存在する
- 細かいユースケースに対応する書き方を調べるのに時間がかかる
- まとめて解説している記事がそれほど多くない
という問題が存在していると感じています。
また、標準テンプレートエンジンの仕組みに触れてみるきっかけは業務で利用しているだけだと多くはないので、年の終わりのアドベントカレンダーの機会を利用して、基本的な事項から少し深い所まで一度整理を行いたいと思い至りました。
基本的な利用法
本節は基本的な利用法を並べる形になるので、慣れている方は読み飛ばしていただいて構いません。
変数を外から与えてレンダリング
template構造体に名前を与えて New
し、テンプレートとして利用したい文字列を Parse
し、外からレンダリングしたい変数を与えて Execute
を呼ぶことで、外から与えた変数を文字列中に埋め込んで出力することができます。
簡単なコード例として
package mainimport (
"os"
"text/template"
)func main() {
const templateText = "This is sample template dot is : {{.}}\n"tpl, err := template.New("mytemplate").Parse(templateText)
if err != nil {
panic(err) // エラー処理はよしなに
}
// Executeはinterface{}型を受けるので何でも渡せる
err = tpl.Execute(os.Stdout, false)
if err != nil {
panic(err)
}
}を実行すると次の文章が標準出力に出力されます。This is sample template dot is : falseParse の実行時にエラーがあった場合にpanicにする場合は Must を使うとスッキリ書くことができます。Execute の際に引数として渡した変数の値は、テンプレート内の {{ . }} と表記された部分にレンダリングされます。
ここでいう値とは内部的には reflect.ValueOf の評価結果を fmt.Fprint で出力した際の値を指します。
ファイルに定義されたテンプレートを読み込みことや、Executeに構造体やマップを変数として与え、テンプレート内で構造体のフィールドやマップのキーに対応する値を {{ .FieldName }} や {{ .KeyName }} と書くことで表示することもできます。
下記コードのように、何段かネストした構造体もしくはマップを渡すこともできます。
また、マップ、構造体、構造体へのポインタどれであれ渡すことができます(内部的には reflect.Value 型で受け渡されます)。package mainimport (
"fmt"
"os"
"text/template"
)// LanguageReview represents ...
type LanguageReview struct {
Language string
Stars int
}// Game is ...
type Game struct {
User *Player
Enemy *Player
}// Player is ...
type Player struct {
Name string
HP int
}func main() {
fmt.Println("---template No.1---")
t := template.Must(template.ParseFiles("templates/sample1.tmpl"))// pass map to template
err := t.Execute(os.Stdout, map[string]interface{}{
"Language": "Golang",
"Stars": 5,
})
if err != nil {
panic(err)
}// pass struct to template.
err = t.Execute(os.Stdout, LanguageReview{
Language: "Foolang",
Stars: 2,
})
if err != nil {
panic(err)
}
// pass struct pointer to template.
err = t.Execute(os.Stdout, &LanguageReview{
Language: "Moolang",
Stars: 3,
})
if err != nil {
panic(err)
}fmt.Println("---template No.2---")t2 := template.Must(template.ParseFiles("templates/sample2.tmpl"))err = t2.Execute(os.Stdout, map[string]interface{}{
"Game": map[string]interface{}{
"User": map[string]interface{}{
"Name": "Foo",
"HP": 1000,
},
"Enemy": map[string]interface{}{
"Name": "Bar",
"HP": 2000,
},
},
})err = t2.Execute(os.Stdout, map[string]interface{}{
"Game": Game{
User: &Player{
Name: "Moo",
HP: 1000,
},
Enemy: &Player{
Name: "Var",
HP: 3000,
},
}})
if err != nil {
panic(err)
}
}templates/sample1.tmpl{{ .Language }} has {{ .Stars }} stars :yey:.templates/sample2.tmpl{{ .Game.User.Name }} has {{ .Game.User.HP }} HP
{{ .Game.Enemy.Name }} has {{ .Game.Enemy.HP }} HPテンプレート内での変数の定義テンプレート内は変数を定義することができます。
公式ドキュメントに下記の記載があるように、
- A variable name, which is a (possibly empty) alphanumeric string
preceded by a dollar sign, such as
$piOver2
or
$
The result is the value of the variable.
Variables are described below.
$ で始まる名前で変数を定義し利用することができます。
テンプレート内で定義した変数に構造体などを代入することもできます。
先述したマップや構造体へのアクセスと同様に . でキーやフィールドにアクセス可能です。
prefixとして$をつけない変数を定義することはできません(Parseする際にエラーが返ります)。
変数の定義の仕方としては、templates/sample3.tmpl{{ $x := 1 }}
$x is {{ $x }}
{{ $y := false }}
$y is {{ $y }}
{{ $z := .Game.Enemy }}
Enemy's Name : {{ $z.Name }}
Enemy's HP : {{ $z.HP }}package mainimport (
"fmt"
"os"
"text/template"
)// LanguageReview represents ...
type LanguageReview struct {
Language string
Stars int
}// Game is ...
type Game struct {
User *Player
Enemy *Player
}// Player is ...
type Player struct {
Name string
HP int
}func main() {
fmt.Println("---template No.3---")
t := template.Must(template.ParseFiles("templates/sample3.tmpl"))
t.Execute(os.Stdout, map[string]interface{}{
"Game": map[string]interface{}{
"Enemy": map[string]interface{}{
"Name": "Foo",
"HP": 1000,
},
},
})
}といった具合になります。
また、上記コードを実行すると、---template No.3---$x is 1$y is falseEnemy's Name : Foo
Enemy's HP : 1000と、改行がレンダリングされてしまいます。これを避けるためには、{{- } や {{ -}} などの記法を用いる必要があります。
詳しくは template - The Go Programming Languageに記載があります。
{{-
の記法を用いると直前の空白文字(Go言語においてはスペース、タブ、改行コード)が除去される-}}
の記法を用いると直後の空白文字が除去される
差がわかりやすい例として、package mainimport (
"text/template"
"os"
)func main() {
const tplText = `sample template
ex 1) :Da:
{{ if true }} true :Da {{ else }} false {{ end }}ex 2-1) :Nyet:
{{- if false }}
true :Da:
{{- else }}
false :Da:
{{- end }}ex 2-2) :Nyet:
{{ if false -}}
true :Da:
{{ else -}}
false :Nyet:
{{ end -}}ex 2-3)
{{- if false -}}
true :Da:
{{- else -}}
false :Nyet:
{{- end -}}
`
tpl := template.Must(template.New("sample").Parse(tplText))
err := tpl.Execute(os.Stdout, true)
if err != nil {
panic(err)
}
}を実行すると、sample template
ex 1) :Da:
true :Da:ex 2-1) :Nyet:
false :Nyet:ex 2-2) :Nyet:
false :Nyet:
ex 2-3)false :Nyet:が出力されます。変数定義のケースでは、後続する改行は出力にレンダリングされないことが望まれるので、
templates/sample3_2.tmpl{{ $x := 1 -}}
$x is {{ $x }}
{{- $y := false -}}
{{ $z := .Game.Enemy }}
Enemy's Name : {{ $z.Name }}
Enemy's HP : {{ $z.HP }}と -}} の記法を利用することで、---template No.3---
$x is 1
Enemy's Name : Foo
Enemy's HP : 1000と改行をコントロールすることができます。制御構文制御構文として if や range などを用いることができます。ififの用例を示すと、下記のようになります。sample4.tmpl{{ if .X -}}
inside of the first if
{{ end -}}{{- if .Y.V1 -}}
inside of the second if
{{- else -}}
inside of else of the second if
{{ end }}{{- $x := true}}
{{ if $x -}}
inside of the third if
{{ end }}{{- if .Z -}}
z is nil then? # if
{{- else -}}
z is nil then? # else
{{- end -}}{{ if $z }}
this cause panic
// panic: template: sample4.tmpl:19 undefined variable "$z"
{{ end }}if文に限る話ではありませんが、未定義の変数を参照しようとするとpanicします。if文は bool でない式と共に用いることができます。
ifに与えられた式はテンプレートの内部的にtrue/falseのどちらかであるか評価され、条件分岐が実行されます。
内部的にtrueとは、LL言語に慣れている方には馴染み深い、
- 数値であれば0でない
- 文字列やスライスであれば
len
が0でない
であることです。詳しくは src/text/template/exec.go - The Go Programming Language の278行目を見てみてください。
下記にコードの一部を抜粋します。func isTrue(val reflect.Value) (truth, ok bool) {
if !val.IsValid() {
// Something like var x interface{}, never set. It's a form of nil.
return false, true
}
switch val.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
truth = val.Len() > 0
case reflect.Bool:
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
/// 後略if文においてGo言語で言う && や || を用いたい場合は and や or という記法を用いることができます。
前置記法で書くことになるため、慣れないうちは戸惑うかもしれません(僕も初めは?!となりました笑)。
and と or の引数はいくつでも取ることができます。
例えば{{ if and true false }}
:Nyet:
{{ else }}
and :Da:
{{ end }}{{ if or true false false }}
or :Da:
{{ else }}
:Nyet:
{{ end }}{{ if and 1 2 }}
1,2 :Da: yey.
{{ end }}といった具合です。rangeGo言語で書かれたソースコード内でforループにrange句を使えるのとほぼ同じように、
text/templateにおいてもrangeを使うことができます。templates/sample5.tmpl{{ range $i, $v := .Xs }}
{{- $i }}: Name {{ $v.Name }}, HP {{ $v.HP }}
{{ end -}}
{{- range .Xs -}}
Name {{- .Name }}, HP {{ .HP }}
{{ else }}
this will be rendered if len(.XS) == 0
{{ end -}}{{- $str := .Str -}}
{{- range $i, $v := .Xs -}}
{{- $i }}: Name {{ $v.Name }}, HP {{ $v.HP }}
{{ $str }} # this is ok
{{ end -}}{{ range $i, $v := .Xs }}
{{- $i }}: Name {{ $v.Name }}, HP {{ $v.HP }}
{{ . }}
{{ .Str }} # this is not ok... :|
{{ end -}}とテンプレートを定義して、package mainimport (
"fmt"
"os"
"text/template"
)// LanguageReview represents ...
type LanguageReview struct {
Language string
Stars int
}// Game is ...
type Game struct {
User *Player
Enemy *Player
}// Player is ...
type Player struct {
Name string
HP int
}func main() {
fmt.Println("---template No.5---")
t := template.Must(template.ParseFiles("templates/sample5.tmpl"))
t.Execute(os.Stdout, map[string]interface{}{
"Xs": []Player{
Player{Name: "Player1", HP: 300},
Player{Name: "Player2", HP: 500},
Player{Name: "Player3", HP: 600},
Player{Name: "Player4", HP: 300},
},
"Enemy": map[string]Player{
"Enemy1": Player{Name: "Enemy", HP: 100},
"Enemy2": Player{Name: "Enemy", HP: 100},
"Enemy3": Player{Name: "Enemy2", HP: 200},
},
"Str": "string",
})
}のようにスライスやマップ(もしくはチャンネル)をrangeに与えることで、スライスやマップ、もしくはチャンネルの各要素を順に参照するようなループを書くことができます。
ちなみに、ここには初見殺しの罠が存在しています。
上記のサンプルコードでは、最後のrange文の1ループ目の実行でレンダリングが終わってしまいます。0: Name Player1, HP 300
{Player1 300} # . の値
# 何もレンダリングされない エラーもでないこの現象はrangeを含む幾つかの構文の中では .が指すものが切り替わることで起きます。
rangeの中では . はループ中で現在参照している要素に切り替わります。公式ドキュメントにも、dot is set to the successive elements of the array slice, or map and T1 is executedと記載があります。 . が変化するかしないかは各構文にその旨が添えられていますので、目を通しておいたほうがいいでしょう。
例の場合は Player の存在しないキー Str を参照しに行って、内部的にはエラーになり、レンダリングが途中で終了します。
筆者も初見でこの罠にハマり「???」と思いながら試行錯誤を繰り返しました。関数の呼び出しtext/template では、
- テンプレートに標準で定義されている関数の呼び出し
- テンプレートに独自定義した関数の呼び出し
- 受け渡した構造体に定義されている関数
を呼ぶことが出来ます。テンプレートで提供されている関数標準で提供されている関数に関しては、
src/text/template/funcs.go - The Go Programming Language の builtins という 変数に定義されています。var builtins = FuncMap{
"and": and,
"call": call,
"html": HTMLEscaper,
"index": index,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,// Comparisons
"eq": eq, // ==
"ge": ge, // >=
"gt": gt, // >
"le": le, // <=
"lt": lt, // <
"ne": ne, // !=
}if文の節でご紹介したand や orの正体は実は、if文とセットで用いる単なる記法ではなく、テンプレート内で利用可能な関数なのです。
and が関数であることと、その定義が、// and computes the Boolean AND of its arguments, returning
// the first false argument it encounters, or the last argument.
func and(arg0 interface{}, args ...interface{}) interface{} {
if !truth(arg0) {
return arg0
}
for i := range args {
arg0 = args[i]
if !truth(arg0) {
break
}
}
return arg0
}となっていることからも、
- 前置記法が要求される理由(関数呼び出しなので)
- 引数をいくつでもとれること
がスッキリ理解できるように思えます。
標準ライブラリのコードはGoのインストールパスのsrc下に存在しています。
上記に限らず、標準で提供されている関数の用法や「この値trueになるんだっけな?」等疑問が生じた際、実際に見に行くことでスッキリ解消することができます。エディタによっては定義ジャンプを利用することもできます。文法が薄めに作られているので、標準ライブラリを見に行っても知らない文法が出てこない(=読みやすい)のはGo言語の非常に良いところですね。幾つかの関数の用法lenやindexはスライス、チャンネル、マップに用いることができて便利です。
マップに渡した構造体のメソッドを読んだ返り値のスライス、チャンネル、マップにも適用することができます。
簡単な例としてはpackage mainimport (
"fmt"
"os"
"text/template"
)type Player struct {
Name string
HP int
}func (p *Player) JobList() []string {
return []string{
"Magician",
"Priest",
"Knight",
"Holy Knight",
}
}func main() {
fmt.Println("---template No.8---")
t := template.Must(template.ParseFiles("templates/sample8.tmpl"))
err := t.Execute(os.Stdout, map[string]interface{}{
"SampleSlice": []int{
1, 2, 3, 4, 5,
},
"SampleMap": map[int]string{
1: "uno",
2: "dos",
3: "tres",
},
"P1": &Player{
Name: "Player 1",
HP: 2000,
},
})
if err != nil {
panic(err)
}
}と、テンプレートとして、Hi, :).
length of sample slice is {{ len .SampleSlice }}.
{{ $ss := .SampleSlice }}
length of $ss is {{ len $ss }}length of sample map is {{ .SampleMap }}.
P1's JobList length is {{ len .P1.JobList }}index "SampleSlice" of . is {{ index . "SampleSlice" }}
the 2nd elemnt of index "SampleSlice" of . is {{ index . "SampleSlice" 1 }}を用いて実行してみると、結果から振る舞いが読み取れると思います。
他の関数と同様、未定義の変数を引数にするとpanicします。独自定義の関数template構造体はFuncMapというフィールドを持っていて、独自の関数を定義し、FuncMapに追加することでテンプレート内で利用することができます。
例として、簡単に偶数判定の関数等を定義してみると、package mainimport (
"fmt"
"os"
"text/template"
)func main() {
fmt.Println("---template No.7---")
// FuncMap の型は map[string]interface{}
funcMap := template.FuncMap{
"isEven": IsEven,
"isOverTen": IsOverTen,
"double": Double,
}
t := template.Must(template.New("sample7.tmpl").Funcs(funcMap).ParseFiles("templates/sample7.tmpl"))
err := t.Execute(os.Stdout, map[string]interface{}{
"Num": 3,
"User": map[string]interface{}{
"ID": 1000,
},
})
if err != nil {
panic(err)
}
}// IsEven returns true when i is even number. triial comment haha :).
func IsEven(i int) bool {
return i%2 == 0
}// IsOverTen is ... (please just read :)).
func IsOverTen(i int) bool {
return i > 10
}// Double returns 2x value of given i.
func Double(i int) int {
return i * 2
}sample7.tmpl{{ isEven .Num }}
{{ isEven .User.ID }}2x value is Even .. :).
{{ double .Num | isEven }}のようにテンプレート内で利用することができます。実行すると、--template No.7---
false
true2x value is Even .. :).
trueを出力することができます。結果を | でパイプして別の関数に渡すこともできます。
日時のフォーマットを整える等、実際に使っているとテンプレート側で関数を定義して行いたい処理は沢山あると思うので、その時はFuncMapを自分で定義してあげる必要があります。足し算引き算くらいは標準でやらせてほしいという心の声を発してしまうことはあります ?。FuncMapの定義を確認するFuncMapの定義はsrc/text/template/funcs.go - The Go Programming Language
に書かれています。
定義および実際に関数を評価している部分を見ると色々見えてきます。// FuncMap is the type of the map defining the mapping from names to functions.
// Each function must have either a single return value, or two return values of
// which the second has type error. In that case, if the second (error)
// return value evaluates to non-nil during execution, execution terminates and
// Execute returns that error.
type FuncMap map[string]interface{}返り値は1つもしくは2つ、2つ目の型がerror型であればハンドリングしてくれます。
ハンドリングする箇所はsrc/text/template/exec.goに定義されている evalFunctionが呼び出しているevalCallに存在しています。// funが評価対象の関数
result := fun.Call(argv)
// If we have an error that is not nil, stop execution and return that error to the caller.
if len(result) == 2 && !result[1].IsNil() {
s.at(node)
s.errorf("error calling %s: %s", name, result[1].Interface().(error))
}
return result[0]エラーを定義しておくと(panicしてしまいますが)text/template側がハンドリングしてくれるので、状況に応じて定義しておくと良いように見えます。少し便利なライブラリパッケージ管理ツールである glide を作っている Masterminds · GitHubがsprigというライブラリを提供しています。
こちらのパッケージにはhasPrefixやjoinなどstringsパッケージの関数が定義されていたり、空文字が入力された時にdefault値をレンダリングしておきたい場合のdefault関数など、よく使いそうな関数が定義されています。
詳しくはGodocを参照してみてください。
text/templateでもhtml/templateどちらもで使うことができ、各種関数を自分で1つ1つ定義していく時間が惜しい時は重宝します。用例としては、package mainimport (
"fmt"
"os"
"text/template""github.com/Masterminds/sprig"
)func main() {
fmt.Println("---template No.9---")tpl := template.Must(
template.New("sample9.tmpl").Funcs(sprig.TxtFuncMap()).ParseFiles("templates/sample9.tmpl"),
)err := tpl.Execute(os.Stdout, map[string]interface{}{
"String1": "Hello, :yey: sprig FuncMap",
"String2": "使えそうな関数が定義されています",
"truncNum": 9,
})
if err != nil {
panic(err)
}}Hello :yey:, sprig.- sample in README
{{ "hello!" | upper | repeat 5 }}truncate string (English)
- {{ trunc .truncNum .String1 }}
truncate string (Japanese) # (注)文字数でtruncateしてくれない...
- {{ trunc .truncNum .String2 }}を実行すると、---template No.9---
Hello :), sprig.- sample in README
HELLO!HELLO!HELLO!HELLO!HELLO!truncate string (English)
- Hello, :)
truncate string (Japanese)
- 使えそが出力されます。
日本語の扱い周りは注意が必要ですが、十分実用に耐えるパッケージだと思います。テンプレートの内部の仕組みここから先は、Template構造体がどういった構造体なのかソースコードを眺めて軽く理解したいと思います。Template構造体の宣言や生成:文字列→木構造への変換手始めにNewする箇所やTemplateを生成する箇所を眺めてみたいと思います。
まず、Template構造体の宣言を確認すると、parse.Treeとcommon が埋め込まれていることが分かります。// Template is the representation of a parsed template. The *parse.Tree
// field is exported only for use by html/template and should be treated
// as unexported by all other clients.
type Template struct {
name string
*parse.Tree
*common
leftDelim string
rightDelim string
}定義に現れるparseパッケージは、text/templateもしくはhtml/template内部のデータ構造を扱うための標準パッケージです。
テンプレートの文字列を構造体に変換するParse関数の定義を確認すると、func (t *Template) Parse(text string) (*Template, error) {
t.init()
t.muFuncs.RLock()
trees, err := parse.Parse(t.name, text, t.leftDelim, t.rightDelim, t.parseFuncs, builtins)parse.Parseを内部で呼び出していることが分かります。
parse.Parse 関数の返り値は(treeSet map[string]*Tree, err error)でこのTree構造体自体の定義文を確認してみると、// Tree is the representation of a single parsed template.
type Tree struct {
Name string // name of the template represented by the tree.
ParseName string // name of the top-level template during parsing, for error messages.
// 略
lex *lexer
token [3]item // three-token lookahead for parser.
peekCount int
// 略
treeSet map[string]*Tree
}と型宣言の中に宣言している型のポインタ型が含まれており、ある意味再帰的に型が定義されていることが見て取れます。
木構造を扱う場合には、構造体自身のフィールドに自身と同じ型の要素をもたせる必要があり、
Code as Art: Binary tree and some generic tricks with golangの記事にあるように、自身の型へのポインタ型を利用することで再帰的に定義を行うことができます。ポインタ型で定義せず、type errBinaryTree struct {
node interface{}
left errBinaryTree
right errBinaryTree
}のように書くと、コンパイルエラーになります。invalid recursive type errBinaryTree調べていて気がついたこととしては、型定義についてのGo言語の言語仕様の記述の中にも、例として Tree 構造体が定義されています。
The Go Programming Language Specification - The Go Programming Language
下記のブログやStackOverFlowで言及があるように、ポインタ型でないとゼロ値を決定する際や確保するべきメモリのサイズを決定するという観点で困るような気がします(正確なところはしっかり理解しないといけません?)invalid recursive type XXX - podhmo's diary
invalid recursive type in a struct in go - Stack Overflowこの辺りに関しては、別途理解を深める試みを行いたいと思っています。parserの処理parse.Parseが入力として受け取ったテキストを木構造に変換します。
処理を少し追ってみることにします。parse.Parseは内部でTree構造体を新しく生成し、Tree構造体のParse関数func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {
defer t.recover(&err)
t.ParseName = t.Name
t.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)
t.text = text
t.parse()
t.add()
t.stopParse()
return t, nil
}を呼び出します。上に現れるlex関数で字句解析を行う構造体を生成します。
lex構造体の中では、 {{ や }}等のテンプレート内で用いられる区切り文字が設定されていることがわかります。
区切り文字がデフォルトだと {{と}}になることが下記定義からわかります。// lex creates a new scanner for the input string.
func lex(name, input, left, right string) *lexer {
if left == "" {
left = leftDelim
}
if right == "" {
right = rightDelim
}
l := &lexer{
name: name,
input: input,
leftDelim: left,
rightDelim: right,
items: make(chan item),
}
go l.run() // (*1)
return l// 中略const (
leftDelim = "{{"
rightDelim = "}}"
leftComment = "/*"
rightComment = "*/"
)区切り文字に関しては、実はTemplate構造体にleftDelim及びrightDelimを設定するためのDelimsという関数が定義されています。
何らかの事情で{を使うのを避けたい場合などは自分で定義することができます。
具体的なユースケースとしてはAngularJSの区切り文字と衝突してしまう場合などに用います(バックオートを用いることで区切り文字自体を置き換えなくても対応可能です)。字句解析にgoroutineとchannelが使われている前節のサンプルコード中の(*1)ではgoroutineでlexerのrun関数が実行されています。
run関数が文字列を解析し、itemsというchannelに送信します。
字句解析を行っているgoruntineからの送信された「次の要素」は、func (l *lexer) nextItem() item {
item := <-l.items
l.lastPos = item.pos
return item
}と定義された、nextItemという関数を呼び出すことで取得できます。
字句解析と字句解析の結果を木構造のNodeに変換する処理が並行化されています。
字句解析の結果は最終的にはactionという関数で各種構文に対応した木構造のNodeに変換されます。字句解析を行っている箇所の処理は泥臭く読むのが大変ですが、goroutineやchannelなどGoっぽさが現れるなど読んでいて非常に勉強になります。Executeの処理前節に記載したようにParseを行うことで木構造が生成されます。Executeの処理では生成された木構造のNodeに対して順番に変数の置き換えや条件文の処理を行います。
Executeの定義にあるように、func (t *Template) Execute(wr io.Writer, data interface{}) (err error) {
defer errRecover(&err)
value := reflect.ValueOf(data)
// 略
state.walk(value, t.Root) // Rootではvalueが{{ . }}に対応
return
}walkという関数(そのままの名前!)でNodeを順に処理します。range文のお話をした節で{{ . }}の内容が切り替わるという話をしたと思います。その切り替わりの処理自体はrangeに対応するNodeを処理する箇所に下記のように、func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
s.at(r)
defer s.pop(s.mark())
val, _ := indirect(s.evalPipeline(dot, r.Pipe)) // {{ . }}をvalに代入
// mark top of stack before any variables in the body are pushed.
mark := s.mark()
oneIteration := func(index, elem reflect.Value) {
// 略
s.walk(elem, r.List) // {{ . }} をelemに切り替えてwalkを実行
s.pop(mark)
}
switch val.Kind() {
case reflect.Array, reflect.Slice:
if val.Len() == 0 {
break
}
for i := 0; i < val.Len(); i++ {
oneIteration(reflect.ValueOf(i), val.Index(i)) // dotはslice等の各要素に切り替わる
}
returnTemplate構造体を使っていて変数のスコープがどこで切り替わっているのかが気になったら、このように奥の方を追ってみるのが良いと思います。おわりにここまで読んでいただきありがとうございます。
Goの標準テンプレートエンジンについて簡単に整理を試みました。
文中でも何度か書いたように、標準ライブラリのコードを読む時に知らない文法がほとんどでてこない(程度に言語仕様が薄い)のはGoの非常に良い点だと思います。
今後も他の標準ライブラリを読んで得た知見などをproductionコードに反映したり、知見がまとまった段階で公開していきたいです。明日の記事は 太田さん の「angular-cliで始めるAngular2」になります!それではみなさん良いクリスマスを〜