CLOVER🍀

That was when it all began.

Go標準のテンプレートエンジンtext/templateを使ってみる

これは、なにをしたくて書いたもの?

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に書いてあります。

Package template / 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で作成された任意の文字列で、"アクション"と呼ばれるデータの評価や制御構造に使われる構文は{{}}
囲って表現されます。

押さえておいた方がよさそうな要素は以下なのでしょうけど、全部は使えないのでちょっとずつかいつまんで使っていくことに
しましょう。

Text and spaces

Actions

Arguments

Pipelines

Variables

Functions

Nested template definitions

まずはこんな感じで。

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で指定した名前のテンプレートを作成します。

func New

この時点では中身はなく、その後のParseでテンプレートを文字列として渡し、パースしています。

func (*Template) 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)

今回は、評価結果は文字列として取得しました。

func (*Template) Execute

最後に確認。

   assert.Equal(t, "カツオ 磯野", writer.String())

構造体の値を参照する場合、メンバーの名前は大文字で始める必要があります。

Actions

ここですね。

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")
}

このようにテンプレートの評価に失敗する場合は、Executeerrorを返してきます。

テンプレートをファイルとして用意する

テンプレートはファイルとしても用意できます。この場合、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.

func (*Template) ParseFiles

今回のテンプレートファイルは、先ほど文字列で用意したものと同じ内容にしてあります。 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())
}

こちらを使っています。

func ParseFiles

どちらの関数にも言えるのですが、複数のファイルをパースできるようです。その挙動は今回は確認しませんでした…。

これ以降は、テンプレートを文字列で定義して使うことにします。

.は現在の位置に応じた値を指す

.は、現在の位置に応じた値を指します。

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.

Overview

と書くとよくわからない感じなのですが、トップレベルで使うとテンプレートにバインドされた値そのものを指します。

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.

Arguments

Variables

テンプレート上で、変数が定義できます。

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 := .}}

ネストされた要素を参照する

構造体内のネストされた要素(構造体のメンバー)も、参照できます。

Arguments

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}}

マップやスライスを扱う

テンプレート上で、マップやスライス、配列を扱うことができます。

Arguments

まずはマップ。

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(ループ)を使うと、値が.にバインドされます。

Actions

{{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関数を使います。

Functions

{{index $vals 0}}
{{index $vals 1}}
{{index $vals 2}}

rangeを使った場合は、要素そのもの、もしくはインデックスと要素をバインドできます。

{{range  $vals -}}
{{.}}
{{end -}}

----------------------------------------

{{range $index, $element := $vals -}}
{{$index}} = {{$element}}
{{end -}}

- でホワイスペースを削除する

さっきからテンプレートに時々{{ -}}みたいなのが登場するのですが、これはホワイトスペースを削除する効果があります。

Text and spaces

{{- }}であれば指定したアクションより左のホワイトスペース、{{ -}}であれば指定したアクションより右のホワイトスペースを
削除します。左右両方指定しても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())
}

条件分岐

ifelse ifelseを使った条件分岐も書けます。

Actions

andorを使うこともできます。

Functions

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())
}

メソッド呼び出しを行う

メソッド呼び出しも可能です。

Pipelines

引数の指定は、()を使わないことに注意しましょう。

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を試してみました。

最初は使い方がよくわからなかったのですが、動かしたり調べたりしているうちに、だいぶ雰囲気はわかりようになった
気がします。

今回、全部の機能や構文を網羅したわけではないですが、あとはドキュメントを見ながら進めていけるかな、と。