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

このようにテンプレヌトの評䟡に倱敗する堎合は、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.

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

条件分岐

if、else if、elseを䜿った条件分岐も曞けたす。

Actions

andやorを䜿うこずもできたす。

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を詊しおみたした。

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

今回、党郚の機胜や構文を網矅したわけではないですが、あずはドキュメントを芋ながら進めおいけるかな、ず。