Zenn
🍮

Goの標準テンプレートエンジンのチュートリアル

2025/03/31に公開

はじめに

Goの標準ライブラリはとても充実していて、テンプレートエンジンもtext/templatehtml/template パッケージとして含まれています。
最近ではフロントのフレームワークが定着しているため、HTMLの生成で活用するケースは見かけなくなっていたとしても、コードを生成するような場面などでは依然として強力なツールになっているのではないでしょうか。
ただ、それほど頻繁に使うものではないので、久しぶり使おうとしたら記憶が薄れていて、パッケージドキュメントを読み返して見たものの、なかなか知りたい目的の点と点を結び付けることができず悶絶してしまったり、ありきたりのことをしたいだけだったのに機能が豊富で学習コストが高くついてしまった。なんて、似たような経験をされた方がいたり、いなかったりするかもしれません。
そこで、すべての機能を網羅した厳密な仕様を把握したいわけではなく、既成のコードジェネレーターの大まかな流れを軽く追うために記号の意味を手短に知りたい。
そんな設定で、学習曲線を改善させられるような構成での整理を試みました。

導入

まずはテンプレートを使う全体の流れになります。

main.go
package main

import (
	"bytes"
	"fmt"
	"log"
	"text/template"
)

// テンプレート
const tmpl = `
商品:{{.Name}}
在庫:{{.Qty}}個
`

// テンプレートに与える項目
type vals struct {
	Name string // 商品
	Qty  int    // 在庫
}

func main() {
	// テンプレートの準備
	const tmplName = ""
	t := template.New(tmplName)
	if _, err := t.Parse(tmpl); err != nil {
		log.Fatal(err)
	}

	// テンプレートに与える値
	v := &vals{
		Name: "プリン",
		Qty:  3,
	}

	// 生成
	var generated bytes.Buffer
	if err := t.Execute(&generated, v); err != nil {
		log.Fatal(err)
	}

	fmt.Print(generated.String())
}
  • テンプレートの {{}} で囲われた部分のアクションによって出力が生成される
  • 現在位置の値を示すカーソルは . で表される
    • Executeメソッドの第2引数が . で参照できる
      • 任意の値( any )を設定できるが、実用的なのは構造体(もしくはマップ)。構造体を使う場合はフィールドを公開(大文字始まり)にする
  • . に構造体のフィールド名を続ける
  • *templateTemplateNew したらテンプレートを Parse する。 Execute で生成される
    • テンプレート名は空文字にしても構わないが、 *templateTemplate が複数になる場合は重複しないようにする

例の表記

以降の例はテンプレートに主眼をおいた実装で示していきます。

tmpl.txt
商品:{{.Name}}
在庫:{{.Qty}}個
main.go
package main

import (
	_ "embed"
	"os"
	"text/template"
)

//go:embed tmpl.txt
var tmpl string

func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
  • テンプレートを別ファイルに分離しています
  • template.MustParse でエラーになったときにpanicさせるヘルパー関数です
  • 構造体からマップに変更し、 . に続けるのはマップのキー名になっています
  • 大文字始まりに構造体のときと揃えていますが、マップであれば小文字始まりの文字列をキーに使うこともできます
実行結果
$ go run main.go
商品:プリン
在庫:3個

if

構文

{{if 条件1}}
T1
{{else if 条件2}}
T2
{{else}}
T0
{{end}}
  • 条件が true のときに出力される。structの場合はnilでなければ true 判定になる
  • 条件がゼロ値だったり、サイズや長さが0なら false 判定になる
  • ==!= などの比較演算子は使えない。 eq / ne / lt / le / gt / ge のbooleanを返す組み込み関数を使う
    • arg1 == arg2 であれば arg1 eq arg2 とするのではなく eq arg1 arg2 と関数名、引数の順で スペース区切り にして使う
    • eq 関数だけは特別に引数が可変で、最初の引数が2番目以降のいづれかと等しい場合に true を返すIN条件での判定になる
  • &&|| などの論理演算子も使えない。同様に and / or / not の組み込み関数を使う

tmpl.txt
商品:{{.Name}}{{if .Good}}(おすすめ品){{end}}
在庫:{{if gt .Qty 3}}あり{{else if .Qty}}わずか{{else}}なし{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Good": true,
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン(おすすめ品)
在庫:わずか

with

構文

{{with 値1}}
T1
{{else with 値2}}
T2
{{else}}
T0
{{end}}
  • if の代わりに with を使っても同様の出力になる
  • ブロック内で . に値がセットされる点が異なる

tmpl.txt
商品:{{.Name}}
在庫:{{if .Qty}}{{.Qty}}個{{else}}なし{{end}}
在庫:{{with .Qty}}{{.}}個{{else}}なし{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個
在庫:3個
  • 在庫がどちらも同じ出力になっている

range

構文

{{range 繰返し要素}}
T1
{{end}}
  • 繰返し要素分だけT1が繰返し出力される
  • 繰返し要素は配列、スライス、マップのほか、イテレーター、整数、チャネル
  • ループのブロック内では . に各要素がセットされる
  • {{break}}{{continue}} が使える

tmpl.txt
{{range .}}
商品:{{.Name}}プリン{{if eq "焼" .Name}}(NEW!){{end}}
在庫:{{.Qty}}
{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, []map[string]any{
		{
			"Name": "カスタード",
			"Qty":  15,
		},
		{
			"Name": "焼",
			"Qty":  20,
		},
	})
}
実行結果
$ go run main.go

商品:カスタードプリン
在庫:15

商品:焼プリン(NEW!)
在庫:20

rangeにもelseが使える

{{range 繰返し要素}}
T1
{{else}}
T0
{{end}}
  • 繰返し要素が空のときにT0が出力される

フォーマット

  • fmt.Sprint / fmt.Sprintf / fmt.Sprintln が、それぞれ print / printf / println の組み込み関数で使える

tmpl.txt
商品:{{.Name}}
在庫:{{ printf "%d個" .Qty }}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個

チェイン( |

  • | を使って 最後の引数 を渡すことができる

tmpl.txt
商品:{{.Name}}
在庫:{{ .Qty | printf "%d個" }}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個

トリム

  • {{{{- にすることで直前の空白文字をトリムできる
  • }}-}} にすることで直後の空白文字をトリムできる
  • 空白文字はスペース/タブ/改行コード

トリムなし

tmpl.txt
商品:{{.Name}}
在庫:
{{if gt .Qty 3}}
あり
{{else if .Qty}}
わずか
{{else}}
なし
{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:

わずか

  • テンプレート側で改行していると出力にも影響してしまう

トリムあり

tmpl.txt
商品:{{.Name}}
在庫:
{{- if gt .Qty 3 -}}
あり
{{- else if .Qty -}}
わずか
{{- else -}}
なし
{{- end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
$ go run main.go:実行結果
商品:プリン
在庫:わずか
  • 出力のレイアウトを整えることができる

コメント

構文

{{/* コメント */}}
{{- /* コメント */ -}}

変数

  • テンプレート内で $ 始まりの英数字の文字列を変数として宣言できる
  • 文字列には空文字も使えるので $ だけの変数も有効
  • := で宣言し、以降 = で代入できる

tmpl.txt
商品:{{$ := .Name}}{{$}}
在庫:{{$s := printf "%d個" .Qty}}{{$s}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個

例(if)

  • ifで変数を宣言できる
tmpl.txt
商品:{{.Name}}
在庫:{{if $s := .Qty}}{{$s}}個{{else}}なし{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name": "プリン",
		"Qty":  3,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個

例(配列、スライス)

  • rangeでインデックスと要素の変数を宣言できる
  • 組み込み関数で index / slice / len が用意されている
tmpl.txt
{{- $header := index . 0 -}}{{/* 先頭はヘッダーを取得する */}}
{{- $list := slice . 1 (len .) -}}{{/* 2番目から最後までのサブスライスを作る */}}
{{range $i, $v := $list}}
{{$header.Name}}:{{(index $list $i).Name}}
{{$header.Qty}}:{{$v.Qty}}
{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, []map[string]any{
		{
			"Name": "商品",
			"Qty":  "在庫",
		},
		{
			"Name": "カスタードプリン",
			"Qty":  15,
		},
		{
			"Name": "焼プリン",
			"Qty":  20,
		},
	})
}
実行結果
$ go run main.go


商品:カスタードプリン
在庫:15

商品:焼プリン
在庫:20

例(マップ)

  • rangeでキーと値の変数を宣言できる
tmpl.txt
{{range $k, $v := .}}
商品:{{$k}}
在庫:{{$v}}
{{end}}
main.go
func main() {
	template.Must(template.New("").Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"カスタードプリン": 15,
		"焼プリン":     20,
	})
}
実行結果
$ go run main.go

商品:カスタードプリン
在庫:15

商品:焼プリン
在庫:20

カスタム関数

  • テンプレート内で呼び出せる関数を Funcs メソッドで追加できる
tmpl.txt
商品:{{.Name}}
在庫:{{.Qty}}個
賞味期限:{{.BestBy.Format "2006年1月2日"}}{{/* メソッドを呼び出せる */}}
賞味期限:{{.BestBy | fmtBestBy}}{{/* 和歴はカスタム関数を呼び出す */}}
main.go
package main

import (
	_ "embed"
	"fmt"
	"os"
	"text/template"
	"time"
)

//go:embed tmpl.txt
var tmpl string

func formatDate(t *time.Time) string {
	return fmt.Sprintf("令和%d年%d月%d日", t.Year()-2018, t.Month(), t.Day())
}

func main() {
	bestBy := time.Date(2025, time.April, 6, 0, 0, 0, 0, time.FixedZone("JST", 9*60*60))
	template.Must(template.New("").Funcs(
		template.FuncMap{
			"fmtBestBy": formatDate,
		},
	).Parse(tmpl)).Execute(os.Stdout, map[string]any{
		"Name":   "プリン",
		"Qty":    3,
		"BestBy": &bestBy,
	})
}
実行結果
$ go run main.go
商品:プリン
在庫:3個
賞味期限:2025年4月6日
賞味期限:令和7年4月6日

組み込み関数の一覧

https://github.com/golang/go/blob/go1.24.1/src/text/template/funcs.go#L41-L61

おわりに

gitの履歴が確かならば、自分で書いたことになってる。なぜかテンプレート部分の記憶がない。
もはや過去の自分は、gitの別人なので、また構文や記号の意味を忘れて参照することになりそうな予感がします。
「誰かのために、自分のために」、パッケージドキュメントへの橋渡しとして役立つことを願いながら、この辺りで締めたいと思います。

Discussion

ログインするとコメントできます