これは、なにをしたくて書いたもの?
Goのテンプレートエンジンを調べてみようかなと思ったのですが、標準ライブラリにあるようなので、こちらを試して
みることにしました。
Goの標準ライブラリにあるテンプレートエンジン
text/template
と、html/template
の2種類があるようです。
template - The Go Programming Language
template - The Go Programming Language
名前から想像はつきますが、text/template
はテキスト生成のためのテンプレートエンジンで、html/template
はHTML生成のための
テンプレートエンジンです。
HTML向けのエスケープなどが効くようなので、HTML生成に使いたい時はhtml/template
を使いましょう。
今回はtext/template
を使います。
環境
今回の環境は、こちら。
$ go version go version go1.15.6 linux/amd64
モジュールの初期化。
$ go mod init text-template-example go: creating new go.mod: module text-template-example
動作確認には、testifyを使うことにします。
go.mod
module text-template-example go 1.15 require github.com/stretchr/testify v1.6.1
テストコードの雛形
動作確認は、テストコードで行っていきます。雛形は、こんな感じで用意。
template_test.go
package main import ( "fmt" "strings" "testing" "text/template" "github.com/stretchr/testify/assert" ) // ここにテストを書く!!
text/templateを使う
では、text/template
を使ってみましょう。
template - The Go Programming Language
使い方の基本的なイメージはOverviewに書いてあります。
type Inventory struct { Material string Count uint } sweaters := Inventory{"wool", 17} tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}") if err != nil { panic(err) } err = tmpl.Execute(os.Stdout, sweaters) if err != nil { panic(err) }
テンプレートはUTF-8で作成された任意の文字列で、"アクション"と呼ばれるデータの評価や制御構造に使われる構文は{{
と}}
で
囲って表現されます。
押さえておいた方がよさそうな要素は以下なのでしょうけど、全部は使えないのでちょっとずつかいつまんで使っていくことに
しましょう。
まずはこんな感じで。
func TestTextTemplateGettingStarted(t *testing.T) { type Person struct { FirstName string LastName string } person := &Person{FirstName: "カツオ", LastName: "磯野"} tmpl, err := template.New("test_template").Parse("{{.FirstName}} {{.LastName}}") if err != nil { t.Fatal(err) } writer := new(strings.Builder) tmpl.Execute(writer, person) assert.Equal(t, "カツオ 磯野", writer.String()) }
New
で指定した名前のテンプレートを作成します。
この時点では中身はなく、その後のParse
でテンプレートを文字列として渡し、パースしています。
このあたりですね。テンプレートの構文が誤っていると、error
が返ってきます。
tmpl, err := template.New("test_template").Parse("{{.FirstName}} {{.LastName}}") if err != nil { t.Fatal(err) }
で、テンプレートに値をバインドして評価します。
writer := new(strings.Builder)
tmpl.Execute(writer, person)
今回は、評価結果は文字列として取得しました。
最後に確認。
assert.Equal(t, "カツオ 磯野", writer.String())
構造体の値を参照する場合、メンバーの名前は大文字で始める必要があります。
ここですね。
Although the key must be an alphanumeric identifier, unlike with field names they do not need to start with an upper case letter.
小文字にすると、テンプレートの評価時に参照できずにエラーになります。
func TestTextTemplateGettingStartedFailed(t *testing.T) { type Person struct { firstName string lastName string } person := &Person{firstName: "カツオ", lastName: "磯野"} tmpl, _ := template.New("test_template").Parse("{{.firstName}} {{.fastName}}") writer := new(strings.Builder) err := tmpl.Execute(writer, person) assert.Contains(t, err.Error(), "executing \"test_template\" at <.firstName>: firstName is an unexported field of struct type *main.Person") }
このようにテンプレートの評価に失敗する場合は、Execute
がerror
を返してきます。
テンプレートをファイルとして用意する
テンプレートはファイルとしても用意できます。この場合、ParseFiles
を使います。
func TestTextTemplateFromFile(t *testing.T) { type Person struct { FirstName string LastName string } person := &Person{FirstName: "カツオ", LastName: "磯野"} tmpl, _ := template.New("person.tmpl").ParseFiles("testdata/person.tmpl") writer := new(strings.Builder) tmpl.Execute(writer, person) assert.Equal(t, "カツオ 磯野", writer.String()) }
テンプレートの名前は、「ファイル名」になるようです。New
で指定している部分は、ファイル名と合わせた方がよさそうです。
Since the templates created by ParseFiles are named by the base names of the argument files, t should usually have the name of one of the (base) names of the files. If it does not, depending on t's contents before calling ParseFiles, t.Execute may fail.
今回のテンプレートファイルは、先ほど文字列で用意したものと同じ内容にしてあります。 testdata/person.tmpl
{{.FirstName}} {{.LastName}}
また、ファイルの場合はNew
を使わずにいきなりParseFiles
することもできます。
func TestTextTemplateFromFileSimply(t *testing.T) { type Person struct { FirstName string LastName string } person := &Person{FirstName: "カツオ", LastName: "磯野"} tmpl, _ := template.ParseFiles("testdata/person.tmpl") writer := new(strings.Builder) tmpl.Execute(writer, person) assert.Equal(t, "カツオ 磯野", writer.String()) }
こちらを使っています。
どちらの関数にも言えるのですが、複数のファイルをパースできるようです。その挙動は今回は確認しませんでした…。
これ以降は、テンプレートを文字列で定義して使うことにします。
.は現在の位置に応じた値を指す
.
は、現在の位置に応じた値を指します。
Execution of the template walks the structure and sets the cursor, represented by a period '.' and called "dot", to the value at the current location in the structure as execution proceeds.
と書くとよくわからない感じなのですが、トップレベルで使うとテンプレートにバインドされた値そのものを指します。
func TestDot(t *testing.T) { tmpl, _ := template.New("test_template").Parse("Hello {{.}}!!") writer := new(strings.Builder) tmpl.Execute(writer, "World") assert.Equal(t, "Hello World!!", writer.String()) }
今回の指定は、こちら。
Hello {{.}}!!
ループ(後述)で使ったりすると、.
が指す値が変わっていきます。
なので、.
自体の説明はドキュメント上はこのような表現になっています。
The result is the value of dot.
Variables
テンプレート上で、変数が定義できます。
$
を使って表現します。宣言時は:=
で値を指定し、すでに宣言されている変数に値を再割当てする時は=
を使います。
通常の変数と同じですね。
func TestVariable(t *testing.T) { tmpl, _ := template.New("test_template").Parse(` {{$val := .}} Hello {{$val}}!!`) writer := new(strings.Builder) tmpl.Execute(writer, "World") assert.Equal(t, ` Hello World!!`, writer.String()) }
今回は、.
を変数$val
に割り当てました。
{{$val := .}}
ネストされた要素を参照する
構造体内のネストされた要素(構造体のメンバー)も、参照できます。
func TestNestedStruct(t *testing.T) { type Inner struct { InnerField string } type Outer struct { OuterField string Inner *Inner } inner := &Inner{InnerField: "Bar"} outer := &Outer{OuterField: "Foo", Inner: inner} tmpl, _ := template.New("test_template").Parse("{{.OuterField}} {{.Inner.InnerField}}") writer := new(strings.Builder) tmpl.Execute(writer, outer) assert.Equal(t, "Foo Bar", writer.String()) }
この部分ですね。
{{.Inner.InnerField}}
マップやスライスを扱う
テンプレート上で、マップやスライス、配列を扱うことができます。
まずはマップ。
func TestMap(t *testing.T) { m := map[string]int{ "key1": 1, "key2": 2, "key3": 3, } tmpl, _ := template.New("test_template").Parse(` key1 = {{.key1}} key2 = {{.key2}} key3 = {{.key3}} ---------------------------------------- {{range . -}} {{.}} {{end -}} ---------------------------------------- {{range $key, $value := . -}} {{$key}} = {{$value}} {{end -}} `) writer := new(strings.Builder) tmpl.Execute(writer, m) assert.Equal(t, ` key1 = 1 key2 = 2 key3 = 3 ---------------------------------------- 1 2 3 ---------------------------------------- key1 = 1 key2 = 2 key3 = 3 `, writer.String()) }
.キー
で値を参照できます。
key1 = {{.key1}} key2 = {{.key2}} key3 = {{.key3}}
range
(ループ)を使うと、値が.
にバインドされます。
{{range . -}}
{{.}}
{{end -}}
range
で、キーと値を取り出すことも可能です。
{{range $key, $value := . -}}
{{$key}} = {{$value}}
{{end -}}
スライス。
func TestSlice(t *testing.T) { values := []string{"value1", "value2", "value3"} tmpl, _ := template.New("test_template").Parse(` {{$vals := .}} {{index $vals 0}} {{index $vals 1}} {{index $vals 2}} ---------------------------------------- {{range $vals -}} {{.}} {{end -}} ---------------------------------------- {{range $index, $element := $vals -}} {{$index}} = {{$element}} {{end -}} `) writer := new(strings.Builder) tmpl.Execute(writer, values) assert.Equal(t, ` value1 value2 value3 ---------------------------------------- value1 value2 value3 ---------------------------------------- 0 = value1 1 = value2 2 = value3 `, writer.String()) }
ちょっと見づらかったので、.
を変数にバインドしました。
{{$vals := .}}
スライスの要素を、インデックスを指定して参照するにはindex
関数を使います。
{{index $vals 0}} {{index $vals 1}} {{index $vals 2}}
range
を使った場合は、要素そのもの、もしくはインデックスと要素をバインドできます。
{{range $vals -}} {{.}} {{end -}} ---------------------------------------- {{range $index, $element := $vals -}} {{$index}} = {{$element}} {{end -}}
- でホワイスペースを削除する
さっきからテンプレートに時々{{ -}}
みたいなのが登場するのですが、これはホワイトスペースを削除する効果があります。
{{- }}
であれば指定したアクションより左のホワイトスペース、{{ -}}
であれば指定したアクションより右のホワイトスペースを
削除します。左右両方指定してもOKです。
また、ここでいう"ホワイトスペース"というのはGoと同じで、スペース、水平タブ、CR、LFです。
たとえば、こんな感じにスペースや改行が大量に入ったテンプレートを使っても-
と組み合わせることで一気に削除されます。
func TestTrimWhiteSpaceBetweenAction(t *testing.T) { m := map[string]string{ "word1": "Hello", "word2": "World", } tmpl, _ := template.New("test_template").Parse(`| {{- .word1}} | | {{ .word2 -}} | {{ .word1 -}} {{- .word2}}`) writer := new(strings.Builder) tmpl.Execute(writer, m) assert.Equal(t, "|Hello | | World| HelloWorld", writer.String()) }
また、削除されるホワイトスペースは、あくまで{{-
や-}}
の左右であり、テンプレートに埋め込んだ値のホワイトスペースが
削除されるわけではありません。
func TestTrimWhiteSpaceBetweenAction2(t *testing.T) { m := map[string]string{ "word1": " Hello ", "word2": " World ", } tmpl, _ := template.New("test_template").Parse(`{{ .word1 -}}{{- .word2}}`) writer := new(strings.Builder) tmpl.Execute(writer, m) assert.Equal(t, " Hello World ", writer.String()) }
条件分岐
if
、else if
、else
を使った条件分岐も書けます。
and
やor
を使うこともできます。
func TestBoolConditional(t *testing.T) { type Sample struct { Flag1 bool Flag2 bool Flag3 bool Message string } s := &Sample{Flag1: true, Flag2: true, Flag3: false, Message: "Hello World!!"} tmpl, _ := template.New("test_template").Parse(` {{if .Flag1 -}} Message = "{{.Message}}" {{end}} {{if .Flag3 -}} Message = {{.Message}} {{else -}} Flag3 is false {{end}} {{if .Flag3 -}} Message = {{.Message}} {{else if .Flag2 -}} Flag2 is true {{end}} {{if and .Flag1 .Flag2 -}} Flag1 and Flag2 are true {{end}} {{if and .Flag1 .Flag3 -}} Flag1 and Flag3 are true {{else -}} Either Flag1 or Flag3 is false {{end}} {{if or .Flag1 .Flag3 -}} Either Flag1 or Flag3 is true {{end -}} `) writer := new(strings.Builder) tmpl.Execute(writer, s) assert.Equal(t, ` Message = "Hello World!!" Flag3 is false Flag2 is true Flag1 and Flag2 are true Either Flag1 or Flag3 is false Either Flag1 or Flag3 is true `, writer.String()) }
メソッド呼び出しを行う
メソッド呼び出しも可能です。
引数の指定は、()
を使わないことに注意しましょう。
type Owner struct { name string } func (o *Owner) SayHello(s string) string { return fmt.Sprintf("Hello %s%s", o.name, s) } func TestMethodCall(t *testing.T) { o := &Owner{name: "カツオ"} tmpl, _ := template.New("test_template").Parse(` Method Call = {{.SayHello "!!"}} `) writer := new(strings.Builder) tmpl.Execute(writer, o) assert.Equal(t, ` Method Call = Hello カツオ!! `, writer.String()) }
まとめ
Go標準のテンプレートエンジンのうち、text/template
を試してみました。
最初は使い方がよくわからなかったのですが、動かしたり調べたりしているうちに、だいぶ雰囲気はわかりようになった
気がします。
今回、全部の機能や構文を網羅したわけではないですが、あとはドキュメントを見ながら進めていけるかな、と。