年末なので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 main
import (
"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 : false
Parse
の実行時にエラーがあった場合にpanicにする場合は Must
を使うとスッキリ書くことができます。Execute
の際に引数として渡した変数の値は、テンプレート内の {{ . }}
と表記された部分にレンダリングされます。
ここでいう値とは内部的には reflect.ValueOf
の評価結果を fmt.Fprint
で出力した際の値を指します。
ファイルに定義されたテンプレートを読み込みことや、Execute
に構造体やマップを変数として与え、テンプレート内で構造体のフィールドやマップのキーに対応する値を {{ .FieldName }}
や {{ .KeyName }}
と書くことで表示することもできます。
下記コードのように、何段かネストした構造体もしくはマップを渡すこともできます。
また、マップ、構造体、構造体へのポインタどれであれ渡すことができます(内部的には reflect.Value
型で受け渡されます)。
package main
import (
"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 :).
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 main
import (
"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 false
Enemy's Name : Foo
Enemy's HP : 1000
と、改行がレンダリングされてしまいます。これを避けるためには、
{{- }
や {{ -}}
などの記法を用いる必要があります。
詳しくは template – The Go Programming Languageに記載があります。
{{-
の記法を用いると直前の空白文字(Go言語においてはスペース、タブ、改行コード)が除去される
-}}
の記法を用いると直後の空白文字が除去される
差がわかりやすい例として、
package main
import (
"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
などを用いることができます。
if
ifの用例を示すと、下記のようになります。
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 }}
といった具合です。
range
Go言語で書かれたソースコード内で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 main
import (
"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 main
import (
"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 main
import (
"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
true
2x 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 main
import (
"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, 🙂 sprig FuncMap",
"String2": "使えそうな関数が定義されています",
"truncNum": 9,
})
if err != nil {
panic(err)
}
}
Hello :), 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等の各要素に切り替わる
}
return
Template構造体を使っていて変数のスコープがどこで切り替わっているのかが気になったら、このように奥の方を追ってみるのが良いと思います。
おわりに
ここまで読んでいただきありがとうございます。
Goの標準テンプレートエンジンについて簡単に整理を試みました。
文中でも何度か書いたように、標準ライブラリのコードを読む時に知らない文法がほとんどでてこない(程度に言語仕様が薄い)のはGoの非常に良い点だと思います。
今後も他の標準ライブラリを読んで得た知見などをproductionコードに反映したり、知見がまとまった段階で公開していきたいです。
明日の記事は 太田さん の「angular-cliで始めるAngular2」になります!
それではみなさん良いクリスマスを〜
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!
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 main import ( "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 : false
Parse
の実行時にエラーがあった場合にpanicにする場合は Must
を使うとスッキリ書くことができます。Execute
の際に引数として渡した変数の値は、テンプレート内の {{ . }}
と表記された部分にレンダリングされます。
ここでいう値とは内部的には reflect.ValueOf
の評価結果を fmt.Fprint
で出力した際の値を指します。
ファイルに定義されたテンプレートを読み込みことや、Execute
に構造体やマップを変数として与え、テンプレート内で構造体のフィールドやマップのキーに対応する値を {{ .FieldName }}
や {{ .KeyName }}
と書くことで表示することもできます。
下記コードのように、何段かネストした構造体もしくはマップを渡すこともできます。
また、マップ、構造体、構造体へのポインタどれであれ渡すことができます(内部的には reflect.Value
型で受け渡されます)。
package main import ( "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 :).
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 main import ( "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 false Enemy's Name : Foo Enemy's HP : 1000
と、改行がレンダリングされてしまいます。これを避けるためには、
{{- }
や {{ -}}
などの記法を用いる必要があります。
詳しくは template – The Go Programming Languageに記載があります。
{{-
の記法を用いると直前の空白文字(Go言語においてはスペース、タブ、改行コード)が除去される-}}
の記法を用いると直後の空白文字が除去される
差がわかりやすい例として、
package main import ( "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
などを用いることができます。
if
ifの用例を示すと、下記のようになります。
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 }}
といった具合です。
range
Go言語で書かれたソースコード内で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 main import ( "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 main import ( "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 main import ( "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 true 2x 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 main import ( "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, 🙂 sprig FuncMap", "String2": "使えそうな関数が定義されています", "truncNum": 9, }) if err != nil { panic(err) } }
Hello :), 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等の各要素に切り替わる } return
Template構造体を使っていて変数のスコープがどこで切り替わっているのかが気になったら、このように奥の方を追ってみるのが良いと思います。
おわりに
ここまで読んでいただきありがとうございます。
Goの標準テンプレートエンジンについて簡単に整理を試みました。
文中でも何度か書いたように、標準ライブラリのコードを読む時に知らない文法がほとんどでてこない(程度に言語仕様が薄い)のはGoの非常に良い点だと思います。
今後も他の標準ライブラリを読んで得た知見などをproductionコードに反映したり、知見がまとまった段階で公開していきたいです。
明日の記事は 太田さん の「angular-cliで始めるAngular2」になります!
それではみなさん良いクリスマスを〜
エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!