template
公式ドキュメント:template package - text/template - Go Packages
普段、文字列のフォーマットには fmt.Sprintf 関数を使用することがありますが、これは小さな文字列の処理にのみ適しており、フォーマット動詞を使用してタイプを指定する必要があります。パラメータの命名をサポートせず、複雑な状況の処理もできません。これがテンプレートエンジンが解決しようとする問題です。例えば、バックエンドから直接配信される静的 HTML ページではテンプレートエンジンが必要です。コミュニティには多くの優れたサードパーティ製テンプレートエンジンライブラリがあります。例えば pongo2、sprig、jet などがあります。しかし、この記事で説明する主なトピックは Go 組み込みのテンプレートエンジンライブラリ text/template です。実際の開発では一般的に html/template が使用されます。後者は前者に基づいており、HTML に関する多くのセキュリティ処理が行われています。通常は前者を使用しますが、HTML のテンプレート処理に関わる場合は後者を使用する方が安全です。
クイックスタート
以下にテンプレートエンジンの簡単な使用例を示します。
package main
import (
"fmt"
"os"
"text/template"
)
func main() {
tmpl := `This is the first template string, {{ .message }}`
te, err := template.New("texTmpl").Parse(tmpl)
if err != nil {
fmt.Println(err)
return
}
data := map[string]any{
"message": "hello world!",
}
execErr := te.Execute(os.Stdout, data)
if execErr != nil {
fmt.Println(err)
}
}上記のコードの出力は以下の通りです。
This is the first template string, hello world!ケースコードでは、tmpl はテンプレート文字列で、文字列内の {{ .message }} はテンプレートエンジンのテンプレートパラメータです。まず *Template.Parse メソッドを通じてテンプレート文字列を解析し、
func (t *Template) Parse(text string) (*Template, error)解析に成功した後、*Template.Execute メソッドを通じて data データをテンプレートに適用し、最後に传入された Writer(つまり os.Stdout)に出力します。
func (t *Template) Execute(wr io.Writer, data any) error今後のテンプレートエンジンの使用では、基本的に以下の 3 つのステップがあります。
- テンプレートを取得
- テンプレートを解析
- データをテンプレートに適用
テンプレートエンジンの使用は非常に簡単であることがわかります。もう少し複雑なのはテンプレートエンジンのテンプレート構文で、これがこの記事で主に説明する内容です。
テンプレート構文
パラメータ
Go は二重の中括弧 {{ }} を使用して、テンプレート内でこれがテンプレートパラメータであることを示します。. を使用してルートオブジェクトを示します。ルートオブジェクトは传入された data です。タイプのメンバー変数にアクセスするように、. 記号を使用して変数名を接続し、テンプレート内で対応する値にアクセスできます。例えば
{{ .data }}同名のメンバー変数が存在している必要があります。そうでない場合はエラーが発生します。传入された data は、構造体または map であり、基本タイプ(数字や文字列など)也可以是。この場合、. が表すルートオブジェクトはそれ自身です。中括弧内では、必ずしもルートオブジェクトにアクセスして値を取得する必要はありません。基本タイプのリテラル也可以是。例えば
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}どのタイプでも、最終的には fmt.Sprintf("%s", val) を通じてその文字列表現を取得します。以下の例をご覧ください。
func main() {
out := os.Stdout
tmpl := "data-> {{ . }}\n"
datas := []any{
"hello world!",
6379,
3.1415926,
[]any{1, "2*2", 3.6},
map[string]any{"data": "hello world!"},
struct {
Data string
}{Data: "hello world!"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}
func ExecTmpl(writer io.Writer, tmpl string, data any) error {
parsedTmpl, err := template.New("template").Parse(tmpl)
if err != nil {
return err
}
return parsedTmpl.Execute(writer, data)
}出力は以下の通りです。
data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}出力形式が fmt.Sprintf を直接使用する場合と一致していることがわかります。構造体と map の場合、フィールド名を通じて値にアクセスできます。以下のように示します。
func main() {
out := os.Stdout
tmpl := "data-> {{ .Data }}\n"
datas := []any{
map[string]any{"Data": "hello world!"},
struct {
Data string
}{Data: "hello world!"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}出力は以下の通りです。
data-> hello world!
data-> hello world!スライスと map の場合、特定のインデックスの値にアクセスするための特定の構文は提供されていませんが、関数呼び出しを通じて実装できます。以下のように示します。
func main() {
out := os.Stdout
tmpl := "data-> {{ index . 1}}\n"
datas := []any{
[]any{"first", "second"},
map[int]any{1: "first"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}出力
data-> second
data-> first多次元スライスの場合、以下の方法で対応するインデックスの値にアクセスできます。s[i][j][k] と同等です。
{{ index . i j k }}ネストされた構造体または map の場合、.k1.k2.k3 という方法でアクセスできます。例えば
{{ .person.father.name }}テンプレートパラメータを使用する際、パラメータの前後に - 記号を追加して、パラメータ前後の空白を削除できます。例をご覧ください。
func main() {
out := os.Stdout
tmpl := `{{ .x }} {{ - .op - }} {{ .y }}`
datas := []any{
map[string]any{"x": "10", "op": ">", "y": "2"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}通常、出力結果は 10 > 2 になりますが、op パラメータの前後に - 記号を追加しているため、前後の空白文字はすべて削除されます。したがって、実際の出力は以下の通りです。
10>2中括弧内では、- 記号とパラメータはスペースで区切る必要があることに注意してください。つまり、{{- . -}} という形式である必要があります。例の中で両側に追加のスペースを追加して {{ - . - }} という形式で書いているのは、単に見た目が良いというだけの理由で、実際にはこの構文制限はありません。
コメント
テンプレート構文はコメントをサポートしています。コメントは最終的なテンプレート内では生成されません。構文は以下の通りです。
{{/* this is a comment */}}コメント記号 /* と */ は中括弧に隣接している必要があります。それらの間に他の文字があってはなりません。そうでないと正常に解析できません。例外が 1 つだけあります。それは空白を削除する場合です。
{{- /* this is a comment */ -}}変数
テンプレート内でも変数を宣言できます。$ 記号を使用してこれが変数であることを示し、:= を使用して代入を行います。Go コードと同じです。例は以下の通りです。
{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// 整数代入
{{ $numer := 1 }}
// 浮動小数点数代入
{{ $float := 1.234}}
// 文字列代入
{{ $name := "jack" }}後続の使用では、$ を使用して変数名を接続し、その変数の値にアクセスします。例えば
func main() {
out := os.Stdout
tmpl := `{{ $name := .name }} {{- $name }}`
datas := []any{
map[string]any{"name": "jack"},
}
for _, data := range datas {
err := ExecTmpl(out, tmpl, data)
if err != nil {
panic(err)
}
}
}出力
jack変数は使用する前に宣言する必要があります。そうでないと undefined variable というエラーが表示されます。また、スコープ内でのみ使用できます。
関数
テンプレート自体の構文は実際には多くありません。ほとんどの機能は関数を通じて実装されます。関数呼び出しの形式は、関数名の後にパラメータリストを接続し、スペースを区切り文字として使用します。以下のように示します。
{{ funcname arg1 arg2 arg3 ... }}例えば、以前に使用した index 関数
{{ index .s 1 }}等しいかどうかを比較するための eq 関数
{{ eq 1 2 }}すべての *Template には FuncsMap があり、関数のマップを記録します。
type FuncMap map[string]anyテンプレートを作成する際、text/template.builtins からデフォルトの関数マップテーブルを取得します。以下はすべての組み込み関数です。
| 関数名 | 役割 | 例 |
|---|---|---|
and | AND 演算 | {{ and true false }} |
or | OR 演算 | {{ or true false }} |
not | NOT 演算 | {{ not true }} |
eq | 等しいかどうか | {{ eq 1 2 }} |
ne | 等しくないかどうか | {{ ne 1 2 }} |
lt | 小于 | {{ lt 1 2 }} |
le | 小于等于 | {{ le 1 2 }} |
gt | 大于 | {{ gt 1 2 }} |
ge | 大于等于 | {{ ge 1 2 }} |
len | 長さを返す | {{ len .slice }} |
index | 指定インデックスの要素を取得 | {{ index . 0 }} |
slice | スライス。s[1:2:3] と同等 | {{ slice . 1 2 3 }} |
html | HTML エスケープ | {{ html .name }} |
js | JS エスケープ | {{ js .name }} |
print | fmt.Sprint | {{ print . }} |
printf | fmt.Sprintf | {{ printf "%s" .}} |
println | fmt.Sprintln | {{ println . }} |
urlquery | URL クエリ エスケープ | {{ urlquery .query }} |
これら以外にも、比較的特殊な組み込み関数 call があります。これは Execute 時に传入された data 内の関数を直接呼び出すために使用されます。例えば以下のテンプレート
{{ call .string 1024 }}传入されるデータは以下の通りです。
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}テンプレート内で生成されるのは以下の通りです。
1024: 2048これはカスタム関数を作成する方法の 1 つですが、通常は *Template.Funcs メソッドを使用してカスタム関数を追加することをお勧めします。後者はグローバルに作用し、ルートオブジェクトにバインドする必要がないためです。
func (t *Template) Funcs(funcMap FuncMap) *Templateカスタム関数の戻り値は通常 2 つあります。1 つ目は使用する必要がある戻り値で、2 つ目は error です。例えば、以下のようなカスタム関数があります。
template.FuncMap{
"add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}テンプレート内で直接使用します。
{{ add 1024 }}結果は以下の通りです。
1024 + 1パイプライン
このパイプラインは chan とは異なります。公式ドキュメントでは pipeline と呼ばれています。データを生成する操作はすべて pipeline と呼ばれます。以下のテンプレート操作はすべてパイプライン操作に属します。
{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}Linux に精通している方であれば、パイプライン演算子 | をご存知でしょう。テンプレートでもこのような書き方をサポートしています。パイプライン操作はテンプレート内で頻繁に使用されます。例えば
{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}結果は以下の通りです。
1+1=?後続の with、if、range でも頻繁に使用されます。
with
with 文を使用すると、変数とルートオブジェクトのスコープを制御できます。形式は以下の通りです。
{{ with pipeline }}
text
{{ end }}with はパイプライン操作の戻り値をチェックします。値が空の場合、中央の text テンプレートは生成されません。空の状況を処理したい場合は、with else を使用できます。形式は以下の通りです。
{{ with pipeline }}
text1
{{ else }}
text2
{{ end }}パイプライン操作の戻り値が空の場合、else ブロックのロジックが実行されます。with 文で宣言された変数のスコープは with 文内に限定されます。以下の例をご覧ください。
{{ $name := "mike" }}
{{ with $name := "jack" }}
{{- $name -}}
{{ end }}
{{- $name -}}出力は以下の通りです。スコープが異なるため、これらは 2 つの異なる変数であることは明らかです。
jackmikewith 文を使用すると、スコープ内でルートオブジェクトを書き換えることもできます。以下のように示します。
{{ with .name }}
name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}以下のデータを传入します。
map[string]any{
"name": map[string]any{
"first": "jack",
"second": "bob",
},
"age": 1,
"address": "usa",
}出力
name:bob-jack
age: 1
address: usawith 文の内部では、ルートオブジェクト . がすでに .name になっていることがわかります。
条件
条件文の形式は以下の通りです。
{{ if pipeline }}
text1
{{ else if pipeline }}
text2
{{ else }}
text3
{{ end }}通常のコードを書くのと同じで、非常に理解しやすいです。いくつかの簡単な例をご覧ください。
{{ if eq .lang "en" }}
{{- .content.en -}}
{{ else if eq .lang "zh" }}
{{- .content.zh -}}
{{ else }}
{{- .content.fallback -}}
{{ end }}传入されるデータ
map[string]any{
"lang": "zh",
"content": map[string]any{
"en": "hello, world!",
"zh": "你好,世界!",
"fallback": "hello, world!",
},
}例の中のテンプレートは、传入された言語 lang に基づいて、どのような方法でコンテンツを表示するかを決定します。出力結果
你好,世界!反復
反復文の形式は以下の通りです。range がサポートする pipeline は配列、スライス、map、および channel である必要があります。
{{ range pipeline }}
loop body
{{ end }}else と組み合わせて使用します。長さが 0 の場合、else ブロックのコンテンツが実行されます。
{{ range pipeline }}
loop body
{{ else }}
fallback
{{ end }}これに加えて、break、continue といった操作もサポートしています。例えば
{{ range pipeline }}
{{ if pipeline }}
{{ break }}
{{ end }}
{{ if pipeline }}
{{ continue }}
{{ end }}
loop body
{{ end }}反復の例を 1 つご覧ください。
{{ range $index, $val := . }}
{{- if eq $index 0 }}
{{- continue -}}
{{ end -}}
{{- $index}}: {{ $val }}
{{ end }}传入されるデータ
[]any{1, "2", 3.14},出力
1: 2
2: 3.14map の反復も同様です。
ネスト
1 つのテンプレート内で複数のテンプレートを定義できます。例えば
{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}これらの定義されたテンプレートは、名前が指定されている場合、または template 文を通じて手動で指定されている場合を除き、最終的なテンプレート内では生成されません。
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error以下の例のように示します。
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}以下のデータを传入します。
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}コード
func main() {
out := os.Stdout
tmpl :=
`{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}`
datas := []any{
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
},
}
name := "t1"
for _, data := range datas {
err := ExecTmpl(out, tmpl, name, data)
if err != nil {
panic(err)
}
}
}
func ExecTmpl(writer io.Writer, tmpl string, name string, data any) error {
t := template.New("template")
parsedTmpl, err := t.Parse(tmpl)
if err != nil {
return err
}
return parsedTmpl.ExecuteTemplate(writer, name, data)
}出力
template body 1または、テンプレートを手動で指定することもできます。
{{ define "t1" }}
{{- with .t1 }}
{{- .data -}}
{{ end -}}
{{ end }}
{{ define "t2" }}
{{- with .t2 }}
{{- .data -}}
{{ end}}
{{ end -}}
{{ template "t2" .}}解析時にテンプレート名を指定するかどうかに関係なく、t2 はロードされます。
関連付け
サブテンプレートは、1 つのテンプレート内で複数の名前付きテンプレートを宣言するだけです。関連付けは、外部の複数の名前付き *Template を関連付けます。その後、template 文を使用して指定されたテンプレートを参照します。
{{ tempalte "templateName" pipeline}}pipeline は、関連付けられたテンプレートのルートオブジェクトを必要に応じて指定できます。または、現在のテンプレートのルートオブジェクトを直接传入することもできます。以下のコード例をご覧ください。
func main() {
tmpl1 := `name: {{ .name }}`
tmpl2 := `age: {{ .age }}`
tmpl3 := `Person Info
{{template "t1" .}}
{{template "t2" .}}`
t1, err := template.New("t1").Parse(tmpl1)
if err != nil {
panic(err)
}
t2, err := template.New("t2").Parse(tmpl2)
if err != nil {
panic(err)
}
t3, err := template.New("t3").Parse(tmpl3)
if err != nil {
panic(err)
}
if err := associate(t3, t1, t2); err != nil {
panic(err)
}
err = t3.Execute(os.Stdout, map[string]any{
"name": "jack",
"age": 18,
})
if err != nil {
panic(err)
}
}
func associate(t *template.Template, ts ...*template.Template) error {
for _, tt := range ts {
_, err := t.AddParseTree(tt.Name(), tt.Tree)
if err != nil {
return err
}
}
return nil
}上記のコードでは、t3 が t1 と t2 を関連付けています。*Template.AddParseTree メソッドを使用して関連付けます。
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)最終的なテンプレート生成結果は以下の通りです。
Person Info
name: jack
age: 18スロット
block 文を使用すると、Vue のスロットに似た効果を実現できます。その目的は、特定のテンプレートを再利用することです。使用例を見ると、どのように使用するかがわかります。t1 テンプレート内でスロットを定義します。
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}block 文はスロット内のデフォルトコンテンツを定義できます。後続で他のテンプレートがスロットを使用する際、デフォルトコンテンツが上書きされます。t2 テンプレート内で t1 テンプレートを参照し、define を使用して埋め込むコンテンツを定義します。
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}2 つのテンプレートを関連付けた後、以下のデータを传入します。
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}最終的な出力結果は以下の通りです。
Basic Person Info
name: jack
age: 18
address: usa
school: mitテンプレートファイル
テンプレート構文のケースでは、すべて文字列リテラルをテンプレートとして使用しています。実際の使用状況では、ほとんどの場合テンプレートをファイルに配置します。
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)例えば template.ParseFs は、指定されたファイルシステムから pattern に一致するテンプレートをロードします。以下の例では、embed.FS をファイルシステムとして使用し、3 つのファイルを準備します。
# person.txt
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} {{ end }}
# student.txt
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}
# employee.txt
{{ template "person.txt" . }}
{{ define "slot" }}
company: {{ .company }}
{{ end }}コードは以下の通りです。
import (
"embed"
"os"
"text/template"
)
//go:embed *.txt
var fs embed.FS
func main() {
data := map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}
t1, err := template.ParseFS(fs, "person.txt", "student.txt")
if err != nil {
panic(err)
}
t1.Execute(os.Stdout, data)
t2, err := template.ParseFS(fs, "person.txt", "employee.txt")
if err != nil {
panic(err)
}
t2.Execute(os.Stdout, data)
}出力は以下の通りです。
Basic Person Info
name: jack
age: 18
address: usa
school: mit
Basic Person Info
name: jack
age: 18
address: usa
company: googleこれは非常にシンプルなテンプレートファイルの使用例です。person.txt はスロットファイルとして使用され、他の 2 つはそのコンテンツを再利用し、カスタムized な新しいコンテンツを埋め込みます。以下の 2 つの関数を使用することもできます。
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob はワイルドカードマッチに基づき、ParseFiles はファイル名に基づきます。これらはすべてローカルファイルシステムを使用します。フロントエンドに表示するための html ファイルに使用する場合は、html/template パッケージを使用することをお勧めします。提供される API は text/template と完全に同じですが、html、css、js に対してセキュリティ処理が行われています。
