全力で怠けたい

怠けるために全力を尽くしたいブログ。

Go のテンプレートの使い方のメモ。

Go のテンプレートを軽く調べたのでメモ。

Go のテンプレート

とりあえず使ってみる

Go のテンプレートはこんな感じで使う。

package main

import (
    "os"
    "text/template"
)

type Fruit struct {
    Name  string
    Count int
}

func main() {
    text := `持っている果物: {{.}}`
    tmpl, err := template.New("test").Parse(text)
    if err != nil {
        panic(err)
    }

    fruit := Fruit{
        Name:  "リンゴ",
        Count: 1,
    }
    err = tmpl.Execute(os.Stdout, fruit)
    if err != nil {
        panic(err)
    }
}

// 出力
// 持っている果物: {リンゴ 1}

テンプレートはこんな感じでデータに適用することで実行する。 テンプレートはデータに適用しなくてもいいけど普通はテンプレートを使うときはなにかデータに適用すると思う。

さっきはテンプレートを Fruit 構造体に適用したけどテンプレートは構造体だけじゃなくて配列、スライス、マップ、チャネルとかにも適用できる。 たとえばスライスにはこんな感じで適用する。

package main

import (
    "os"
    "text/template"
)

func main() {
    text := `持っている果物: {{.}}`
    tmpl, err := template.New("test").Parse(text)
    if err != nil {
        panic(err)
    }

    fruits := []Fruit{
        {Name: "リンゴ", Count: 1},
        {Name: "マンゴー", Count: 2},
    }
    err = tmpl.Execute(os.Stdout, fruits)
    if err != nil {
        panic(err)
    }
}

// 出力
// 持っている果物: [{リンゴ 1} {マンゴー 2}]

さっきのコードの {{.}} はテンプレート内で使えるアクションの1つ。 さっきのコードはスライスをそのまま参照して出力したけど、アクションを使うと適用したデータの要素たとえば構造体のフィールドとかマップのキーとかを参照したりテンプレートの実行を制御できる。

さっきのコードのテンプレート内でアクションをこんな感じに使うと構造体のスライスの要素それぞれを参照して出力とかができる。

テンプレートをデータに適用するとデータが走査されていってデータの現在位置は . (ドット) で参照できる。 テンプレート内のアクションを使って条件分岐したりデータを繰り返し処理して目的の出力を得ていくので、だいたいテンプレートの使い方を学ぶ = アクションの使い方を学ぶ感じ。

text := `{{range $index, $element := .}}
{{- .Name}} は {{.Count}} 個あります。
{{end}}`

// 出力
// リンゴ は 1 個あります。
// マンゴー は 2 個あります。

Actions: アクション

アクションは適用したデータの要素たとえば構造体のフィールドとかマップのキーとかを参照したりテンプレートの実行を制御する。

アクションは {{}} で囲んで記述する。 さきほどのコードは {{range $index, $element := .}}, {{.Count}} とか {{end}} がアクション。

テンプレート内のアクションじゃないところはそのまま出力するけど、アクションの左の区切り文字 {{ の直後にマイナス記号と半角スペース (-) を記述するとアクションの直前のテキストの末尾のホワイトスペースをトリムする。同じようにアクションの右の区切り文字 }} の直前に半角スペースとマイナス記号 (-) を記述するとアクションの直後のテキストの末尾のホワイトスペースをトリムする。

text := `{{23}} < {{45}}
{{23 -}} < {{- 45}}`

// 出力
// 23 < 45
// 23<45

アクションはたくさんあるのでそれぞれ書いていく。

comment (コメント)

{{/* a comment */}} と記述したところは出力しない。 コメントは複数行にすることもできる。

text := `{{/* コメントは出力しない */}}
Hello Golang!
{{- /* コメントは
複数行もできる */}}
Hello Template!
`

// 出力
// Hello Golang!
// Hello Template!

{{ pipeline}} はパイプライン (pipeline) をデフォルトで fmt.Print で出力する。 パイプラインは結構いろいろ書けるので後で少し詳しく書く。

text := `{{.}}
{{"リテラルを出力"}}
`

// 出力
// [{リンゴ 1} {マンゴー 2}]
// リテラルを出力

{{if pipeline}} T1 {{end}} は パイプラインがゼロ値のときは何も出力しない。パイプラインがゼロ値じゃなかったら T1 を出力する。

text := `{{if ""}}出力しない{{end}}
{{if "a"}}出力する{{end}}
`

// 出力
// 出力する

{{if pipeline}} T1 {{else}} T0 {{end}} はパイプラインがゼロ値のときは T0 を出力してパイプラインがゼロ値じゃなかったら T0 を出力する。

text := `{{if ""}}出力しない{{else}}"" はゼロ値{{end}}
{{if "a"}}"a" はゼロ値じゃない{{else}}{{end}}
`

// 出力
// "" はゼロ値
// "a" はゼロ値じゃない

{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}{{if}} - {{else} に別の if を入れこんだやつ。

text := `
{{if ""}}出力しない
{{else if 0}}出力しない
{{else}}"" と 0 はゼロ値
{{end}}`

// 出力
// "" と 0 はゼロ値

{{range pipeline}} T1 {{end}} はパイプラインの要素を繰り返し処理する。

パイプラインは配列、スライス、マップ、あるいはチャネルのいずれかである必要がある。 パイプラインの長さゼロがゼロのときは何も出力しない。 パイプライン要素が繰り返し処理するときドット (.) は現在処理している要素を示す。

text := `{{range .}}{{.}}, {{end}}`
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3})

// 出力
// 1, 2, 3, 

{{range pipeline}} T1 {{else}} T0 {{end}}{{range}} と同じだけどパイプラインの要素がないときは T0 を出力する。

text := `{{range .}}{{.}}, {{else}}パイプラインの要素がない{{end}}`
_ = tmpl.Execute(os.Stdout, nil)

// 出力
// パイプラインの要素がない

{{template "name"}} はテンプレートを名前で指定して nil データに適用と実行する。

text := `{{define "foo"}}*** 事前に定義したテンプレート ***{{end}}
{{template "foo"}}
{{template "foo"}}`

// 出力
// *** 事前に定義したテンプレート ***
// *** 事前に定義したテンプレート ***

{{template "name" pipeline}} はテンプレートを名前で指定してドット (.) をパイプラインにセットして適用と実行する。

text := `{{define "foo"}}{{range $index, $element := .}}{{$element}}, {{end}}{{end}}
{{template "foo" .}}
{{template "foo" .}}`
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3})

// 出力
// 1, 2, 3, 
// 1, 2, 3, 

{{block "name" pipeline}} T1 {{end}}{{define "name"}} T1 {{end}}{{template "name" pipeline}} を実行する短縮記法。

text := `{{block "foo" .}}*** 事前に定義したテンプレート ***{{end}}`

// 出力
// *** 事前に定義したテンプレート ***

{{with pipeline}} T1 {{end}}{{if}} とだいたい同じだけどパイプラインがドット (.) にセットして実行する。

text := `{{range $index, $element := . -}}
{{with $element}}{{.}} はゼロ値じゃない{{end}}
{{end}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// 1 はゼロ値じゃない
// 2 はゼロ値じゃない

{{with pipeline}} T1 {{else}} T0 {{end}}{{with pipeline}} T1 {{end}} と同じだけどパイプラインがゼロ値のときは T0 を出力する。

text := `{{range $index, $element := . -}}
{{with $element}}{{.}} はゼロ値じゃない{{else}}ゼロ値{{end}}
{{end}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// ゼロ値
// 1 はゼロ値じゃない
// ゼロ値
// 2 はゼロ値じゃない

Arguments: 引数

Arguments は単なる値でいろいろ入れられる。

定数は untyped constant になって nil は untyped nil になる。

text := `
{{print true}}
{{print "文字列"}}
{{print 1}}
{{print 1.5}}
{{print .}}
{{print nil}}`

_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// true
// 文字列
// 1
// 1.5
// [0 1 0 2]
// <nil>

ドット (.) はその時のドットの値。

text := `{{.}}`
_ = tmpl.Execute(os.Stdout, []int{0, 1, 0, 2})

// 出力
// [0 1 0 2]

$ で始まる変数名。 変数は $ という名前もつけられる。

text := `{{$name := "Alice"}}
{{$ := "Bob"}}
Hello {{$name}}!
Hello {{$}}!`

// 出力
// Hello Alice!
// Hello Bob!

構造体のフィールド。

.Field の形式で記述する。 .Field1.Field2 みたいにネストして参照できて $x.Field1.Field2 みたいに変数のフィールドも参照できる。

text := `{{.Name}} は {{.Count}} 個あります。`
fruit := Fruit{Name: "リンゴ", Count: 1}
_ = tmpl.Execute(os.Stdout, fruit)

// 出力
// リンゴ は 1 個あります。

マップのキー。

.Key の形式で記述する。 .Field1.Key1.Field2.Key2 みたいにネストして参照できて $x.key1.key2 みたいに変数のキーの値も参照できる。

text := `リンゴは {{.リンゴ}} 個あります。
マンゴーは {{.マンゴー}} 個あります。`

m := map[string]int{
    "リンゴ":  1,
    "マンゴー": 3,
}
_ = tmpl.Execute(os.Stdout, m)

// 出力
// リンゴは 1 個あります。
// マンゴーは 3 個あります。

引数のないメソッド。

メソッドは1つか2つの戻り値を返す。 .Field1.Key1.Method1.Field2.Key2.Method2 みたいにネストしてメソッドを呼び出せる。 2つの戻り値を返すときは2つ目の戻り値は error で、2つめの戻り値が nil 以外のときはテンプレートの実行が止まってその erorr が Execute() の戻り値になる。

type Fruit struct {
    Name  string
    Count int
}

func (p Fruit) String() (string, error) {
    s := fmt.Sprintf("%s は %d 個あります。", p.Name, p.Count)
    return s, nil
}

text := `{{.String}}`
fruit := Fruit{Name: "リンゴ", Count: 1}
_ = tmpl.Execute(os.Stdout, fruit)

// 出力
// リンゴ は 1 個あります。

引数のない関数。

引数のない関数の戻り値に関しての振る舞いは引数のないメソッドの戻り値と同じ。

text := `{{print "Hello Template!"}}`

// 出力
// Hello Template!

カッコ ((, )) でグループ化ができる。

{{print or "" "a" "b"}} テンプレートはエラーになるけど以下のように or をカッコで囲ってグループ化するとちゃんと実行する。

text := `{{print (or "" "a" "b")}}`

// 出力
// a

Pipeline: パイプライン

パイプラインはコマンドを パイプ (|) で繋げたもの。

コマンドは単なる値、メソッドあるいは関数で、コマンドは1つまたは2つの値を出力する。 コマンドが2つめの値を出力するとき2つめの出力は erorr で、コマンドの2つめ出力が nil 以外のときはテンプレートの実行が止まってその erorr が Execute() の戻り値になる。

text := `{{"a" | printf "%s"}}
{{"a" | printf "%s" | printf "%q"}}`

// 出力
// a
// "a"

Variables: 変数

変数は {{$variable := pipeline}} の記述で宣言と初期化をする。

宣言済みの変数は {{$variable = pipeline}} の記述で値を割り当てられる。 変数のスコープは変数が {{if}}, {{with}} あるいは {{range}} で宣言すると対応する {{end}} までが変数のスコープになる。

text := `{{$val := "a"}}{{$val}}
{{$val = "b"}}{{$val}}`

// 出力
// a
// b

{{range $index, $element := pipeline}} の記述で pipeline がスライスのときはスライスのインデックスが1つめの変数 $index にセットしてスライスの要素が2つめの変数 $element にセットする。 {{range $index, $element := pipeline}} の記述で pipeline がマッのときはマップのキーが1つめの変数 $index にセットしてマップの要素が2つめの変数 $element にセットする。 このへんの変数名は $index とか $element じゃなくてもいい。

{{range $element := pipeline}} みたいに変数が1つのときは pipeline がスライスのときはスライスの要素が1つめの変数 $element にセットして、pipeline がマップのときはマップの要素が1つめの変数 $element にセットする。このへんの振る舞いは通常の for 文の range の振る舞いと逆なので注意。

Functions: 関数

関数は定義済みの関数とユーザー定義の関数がある。 定義済みの関数をそれぞれ軽く書いて、あとユーザー定義の関数の定義と使い方を書いていく。

定義済みの関数

and は引数のうち最初に見つかったゼロ値を返す。

ゼロ値の引数がないときは最期の引数を返す。

text := `and "a" true 0 1 => {{and "a" true 0 1}}
and "a" true 1 => {{and "a" true 1}}`

// 出力
// and "a" true 0 1 => 0
// and "a" true 1 => 1

call は最初の引数に残りの引数を渡して実行した結果を返す。

最初の引数は1つまたは2つの戻り値を返す関数であること。 関数が返す2つめの戻り値は erorr で、関数の2つめの戻り値が nil 以外のときはテンプレートの実行が止まる。 あと関数の実引数が仮引数と一致しないときもテンプレートの実行が止まる。

text := `{{call .add 1 2}}`

data := map[string]interface{}{
    "add": func(a, b int) int { return a + b },
}
_ = tmpl.Execute(os.Stdout, data)

// 出力
// 3

html は引数を HTML としてエスケープした結果を返す。

text := `{{html "<html>"}}`

// 出力
// &lt;html&gt;

index は最初の引数に残りの引数をインデックスとして適用した結果を返す。

最初の引数は配列、スライスまたはマップである必要がある。

text := `{{index . 2}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// c

slice は最初の引数を残りの引数でスライスした結果を返す。

最初の引数は配列、スライスまたは文字列である必要がある。

text := `{{slice . 1 3}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c", "d"})

js は引数を JavaScript としてエスケープした結果を返す。

text := `{{js "<script>alert('hello');</script>"}}`

// 出力
// \x3Cscript\x3Ealert(\'hello\');\x3C/script\x3E

len は引数の長さを返す。

text := `{{len .}}`
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c", "d"})

// 出力
// 4

not は引数のブール値の否定を返す。

text := `{{not true}}`

// 出力
// false

or は引数のうち最初に見つかった非ゼロ値を返す。

非ゼロ値の引数がないときは最期の引数を返す。

text := `or "" false 1 0 => {{or "" false 1 0}}
or "" false 0 => {{or "" false 0}}`

// 出力
// or "" false 1 0 => 1
// or "" false 0 => 0

print, printlnprintf はそれぞれ fmt.Sprint, fmt.Sprintlnfmt.Sprintfエイリアス

text := `{{print "Hello Template"}}!
{{println "Hello Template"}}!
{{printf "Hello %s!" "Template" }}`

// 出力
// Hello Template!
// Hello Template
// !
// Hello Template!

urlquery は引数をクエリストリングとしてエスケープした結果を返す。

text := `{{urlquery "https://golang.org/?lang=ja"}}`

// 出力
// https%3A%2F%2Fgolang.org%2F%3Flang%3Dja
text := `eq true true => {{eq true true}}
ne true true => {{ne true true}}
lt 1 1 => {{lt 1 1}}
lt 0 1 => {{lt 0 1}}
le 1 1 => {{le 1 1}}
le 0 1 => {{le 0 1}}
gt 1 1 => {{gt 1 1}}
gt 2 1 => {{gt 2 1}}
ge 1 1 => {{ge 1 1}}
ge 2 1 => {{ge 2 1}}`

// 出力
// eq true true => true
// ne true true => false
// lt 1 1 => false
// lt 0 1 => true
// le 1 1 => true
// le 0 1 => true
// gt 1 1 => false
// gt 2 1 => true
// ge 1 1 => true
// ge 2 1 => true

ユーザー定義の関数

ユーザー定義の関数は template.FuncMaptemplate.Funcs() に渡すと使えるようになる。 template.Funcs(funcMap FuncMap)template.Parse() よりも前に呼び出す必要がある。

funcs := map[string]interface{}{
    "sum": func(numbers []int) int {
        total := 0
        for _, v := range numbers {
            total += v
        }
        return total
    },
}
text := `{{sum .}}`
tmpl, err := template.New("test").Funcs(funcs).Parse(text)
_ = tmpl.Execute(os.Stdout, []int{1, 2, 3, 4, 5})

// 出力
// 15

ファイルに保存しているテンプレートを読む

ここまではテンプレートはプログラムの中にハードコードしてたけど実際にテンプレートを使うときはテンプレートはファイルに保存しておいてプログラムからテンプレートを読み込んで使うことが多そう。 ファイルに保存しているテンプレートは template.ParseFiles() とか ParseGlob() で読み込んで使っていく。

tmpl, err := template.ParseFiles("header.template", "main.template", "footer.template")
if err != nil {
    panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "main", nil)
if err != nil {
    panic(err)
}

// 出力
// *** ヘッダー ***
// *** メイン ***
// *** フッター ***

テンプレートを保存してるファイルはこんな感じ。

// header.template
{{define "header"}}*** ヘッダー ***{{end}}

// main.template
{{define "main"}}
{{template "header"}}
*** メイン ***
{{template "footer"}}
{{end}}

// footer.template
{{define "footer"}}*** フッター ***{{end}}

text/template と html/template

text/templatehtml/template は同じインタフェースだけど HTML を出力するときはセキュリティ上の問題から html/template を使うように書いてある。

To generate HTML output, see package html/template, which has the same interface as this package but automatically secures HTML output against certain attacks.

参考ページ