Go言語のテンプレート機能text/templateを使ってみる

はじめに

現在作成中のプログラムでテンプレートを使ってメール送信文面を用意しようと思いました。

Go の組み込みテンプレート機能を使って実現しましたが、基本機能以外にどんな機能があるか知りたいと思い調べてみました。

提供モジュール

template というパッケージを利用します。

組み込みモジュール text の中で提供されており、利用したい場合には text/template を Import する事となります。

特徴

テンプレートとなるテキストデータの中に、埋め込みたいデータを記述できます。

多くの場合、埋め込みデータの型は「構造体」または「マップ」が使われます。

テンプレートファイルの文字コード

UTF-8 を利用します。

埋め込みデータの参照

テンプレート内に {{ と }} で囲んだ部分にデータを埋め込めます。

埋め込んだデータの「フィールド」を . で始まるように記述することで参照できます。

実際にサンプルコードを見たほうが早いので以下にコードを掲載します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"os"
	"text/template"
)

func main() {
	type Inventory struct {
		Material string
		Count    uint
	}

	sweaters := Inventory{"wool", 17}

	templateText := "{{.Count}} items are made of {{.Material}}"

	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, sweaters)
}

エラーハンドリングは省略しています。

main.go というファイル名でコードを保存し実行した結果は以下のようになります。

1
2
$ go run main.go
17 items are made of wool

埋め込みデータの参照以外の機能

リテラルの埋め込み

{{...}} の部分に記述できるのは、埋め込んだ構造体のフィールドやあるいはマップのキーだけではないです。

「評価」できるものであれば何でもよい。

例えば、数値リテラルや文字列リテラルを使うことができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import (
	"os"
	"text/template"
)

func main() {
	templateText := "{{2}} {{\"dollers\"}}"

	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, nil)
}

実行結果は以下のようになります。

1
2 dollers

{{/* ... */}}

コメントとして扱われます。 テンプレートの処理では無視されます。

1
2
# 結果は "こんにちわ  さん"
"こんにちわ {{/* .name */}} さん"

{{- ...}} と {{ ... -}}

デフォルトの挙動では、 先のサンプルコードにおける items are made of という文字列部分はそのまま標準出力に出力されます。

{{...}} の記述に - (マイナス)を左端または右端に埋め込むことで、テンプレートのスペースをトリミングできます。

実際にサンプルコードを見たほうが早いので以下にコードを掲載します。

1
templateText := "{{23 -}} < {{- 45}}"

こちらのテンプレート文字列をパースすると以下のようになります。

1
"23<45"

- が記述された側のテンプレート内のスペースがトリミングされていることがわかります。

ここで言うスペースには「空白」「タブ」「改行」「キャリッジリターン」などが含まれています 。

{{-2}} のように記述するとスペースがトリムされない点に注意が必要です。 これは -2 という数値が指定されたと判断されま s う。 また {{2-}} のようにマイナスをくっつけてしまうとエラーになってしまいます。

{{...}}

最も基本的な使い方になります。

テンプレートから埋め込んだ構造体やマップを参照します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package main

import (
	"os"
	"text/template"
)

func main() {
	data := map[string]interface{}{
		"name": "太郎",
	}

	templateText := "こんにちわ {{ .name }} さん"

	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, data)
}
1
2
$ go run main.go
こんにちわ 太郎 さん

{{if ...}} T1 {{end}}

if 分岐処理を記述できます。

評価された値が空の場合、出力は行われません。 それ以外の場合は T1 が出力されます。

ここで言う「空」とは false / 0 / nil / 長さ 0 の array, slice, map, string になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

import (
	"os"
	"text/template"
)

func main() {
	data := map[string]interface{}{
		"a": "",
		"b": nil,
		"c": []string{},
		"d": map[string]string{},
		"e": 0,
		"f": false,
		"g": true,
		"h": "hello",
	}

	templateText := `
a = {{if .a }}a{{end}}
b = {{if .b }}b{{end}}
c = {{if .c }}c{{end}}
d = {{if .d }}d{{end}}
e = {{if .e }}e{{end}}
f = {{if .f }}f{{end}}
g = {{if .g }}g{{end}}
h = {{if .h }}h{{end}}
`

	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, data)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ go run ./main.go

a =
b =
c =
d =
e =
f =
g = g
h = h

{{if VALUE}} T1 {{else}} T0 {{end}}

if と同じ判定ロジックですが、 if の条件を満たさなかったときには else 以降の値、つまり T0 が出力されます。

{{if VALUE}} T1 {{else if VALUE}} T0 {{end}}

if と同じ判定ロジックですが、 if の条件を満たさなかったときには else if の判定が行われ、条件を満たせば T0 の値が出力されます。

{{range VALUE}} {{.}} {{end}}

VALUE に指定可能なああタイア h array, slice, map, channel になります。

nil やサイズが 0 だった場合には何も出力されません。

要素がセットされていた場合には、 {{range ...}} {{end}} の間の出力が順に行われます。 ループ中は . の指し示す対象がいてレート中の要素(array なら各要素、map なら値)となります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"os"
	"text/template"
)

func main() {
	data := map[string]string{
		"a": "value-a",
		"b": "value-b",
		"c": "value-c",
	}

	templateText := "{{range .}} {{.}} \n{{end}}"

	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, data)
}
1
2
3
4
$ go run main.go
 value-a
 value-b
 value-c

{{break}}

{{range ...}} とセットで使用されます。

他の言語同様、ループ処理を強制終了させることができます。

そのため、 {{if ...}} ともセットで使われることになるでしょう。

{{continue}}

やはり多言語の continue キーワードと同様です。

{{template "name" VALUE}}

template.New().Parse() という形式で毎回テンプレートをパースしている理由がここになります。

テンプレートの中で他のテンプレートを参照できます。

更に他のテンプレートを参照する際に、データを注入できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"os"
	"text/template"
)

func main() {
	tmpl, _ := template.New("hello").Parse("こんにちわ、{{.}}さん!")

	templateText := `-----
{{template "hello" "たろう"}}
{{template "hello" "じろう"}}
{{template "hello" "さぶろう"}}
-----
`
	tmpl, _ = tmpl.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, nil)
}
1
2
3
4
5
6
$ go run main.go
-----
こんにちわ、たろうさん!
こんにちわ、じろうさん!
こんにちわ、さぶろうさん!
-----

{{define "name"}} T1 {{end}}

テンプレートファイルの中で、テンプレートを定義してしまおう、というもの。

必然的に{{template "name" VALUE}} の記法と一緒に使用されることとなります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"os"
	"text/template"
)

func main() {
	templateText := `-----
{{template "hello" "たろう"}}
{{template "hello" "じろう"}}
{{template "hello" "さぶろう"}}
{{define "hello"}}おはましょう、{{.}}さん。{{end}}
-----
`
	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, nil)
}
1
2
3
4
5
6
7
$ go run main.go
-----
おはましょう、たろうさん。
おはましょう、じろうさん。
おはましょう、さぶろうさん。

-----

{{define "name"}} ... {{end}} 前後の文字も出力に含まれているので、上手に記述しないと余計なスペースや改行が出力されてしまいます。

そんなときに便利なのが先に登場した {{- ... -}} の記法です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package main

import (
	"os"
	"text/template"
)

func main() {
	templateText := `-----
{{template "hello" "たろう"}}
{{template "hello" "じろう"}}
{{template "hello" "さぶろう"}}
{{- define "hello" -}}おはましょう、{{.}}さん。{{end}}
-----
`
	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, nil)
}
1
2
3
4
5
6
$ go run main.go
-----
おはましょう、たろうさん。
おはましょう、じろうさん。
おはましょう、さぶろうさん。
-----

不要なスペースが出力されず、見やすいです。

{{with VALUE}} T1 {{end}}

階層が深いデータセットを操作するときに便利です。

. 以降の階層指定を何度も行わないといけない場合にシンプルに記述できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
	"os"
	"text/template"
)

func main() {
	data := map[string]interface{}{
		"a": map[string]interface{}{
			"b": map[string]interface{}{
				"c": "value-c",
				"d": "value-d",
				"e": "value-e",
			},
		},
	}

	tmpl, _ := template.New("hello").Parse("こんにちわ、{{.}}さん!")

	templateText := `
# 個別に書くとこうなる
{{.a.b.c}} {{.a.b.d}} {{.a.b.e}}

# withを使うとこうなる
{{with .a.b -}} {{.c}} {{.d}} {{.e}} {{- end}}
`
	tmpl, _ = tmpl.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, data)
}
1
2
3
4
5
6
7
$ go run main.go

# 個別に書くとこうなる
value-c value-d value-e

# withを使うとこうなる
value-c value-d value-e

コード量は増えていますが、重複が消えているため読みやすく、また DRY 原則に従っています。

メソッド呼び出し

{{...}} の部分でメソッドを呼び出すこともできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"os"
	"strconv"
	"text/template"
)

type human struct {
	name string
	age  int
}

func (h human) Greeting(comic string) string {
	text := "私の名前は" + h.name + "です。年齢は" + strconv.Itoa(h.age) + "歳です。"
	text += "好きな漫画は" + comic + "です。"
	return text
}

func main() {
	data := human{"ゲンゾウ", 43}

	templateText := `
はじめまして。

{{.Greeting "寄生獣"}}

よろしくお願いします。
`
	tmpl, _ := template.New("test").Parse(templateText)
	tmpl.Execute(os.Stdout, data)
}
1
2
3
4
5
6
7
$ go run main.go

はじめまして。

私の名前はゲンゾウです。年齢は43歳です。好きな漫画は寄生獣です。

よろしくお願いします。

ひとこと

text/template の記法は docker コマンドのようなコマンドラインツールで引数を指定する際にも使えたり、覚えておくといろんな状況で便利です。


参考ページ