golangの日記

Go言語を中心にプログラミングについてのブログ

Go言語(golang) テンプレートの使い方

golang.png



text/template と 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. template package - text/template - Go Packages

インターフェースは同じだけど、HTMLの出力が自動的にセキュアになる





目次



基本的な使い方


テキストをパースしてExecute関数の第二引数で値を渡す。
渡した値はテキスト内の {{ }}. として参照することができる。

package main

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

func main() {
    text := "{{ . }}\n"

    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    value := "hello world"

    if err := tpl.Execute(os.Stdout, value); err != nil {
        log.Fatal(err)
    }
    // 出力
    // hello world
}


マップを渡す


テキスト中にある .name.age をそれぞれ map の値で置き換える

package main

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

func main() {
    text := `Name: {{ .name }}
Age: {{ .age }}
`

    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    m := map[string]interface{}{
        "name": "Tanaka",
        "age":  31,
    }

    if err := tpl.Execute(os.Stdout, m); err != nil {
        log.Fatal(err)
    }
    // 出力
    // Name: Tanaka
    // Age: 31
}


構造体を渡す


マップと違って構造体はフィールド名の先頭の文字を大文字にする必要がある

小文字の場合は以下のようなエラーになる
executing "" at <.name>: name is an unexported field of struct type struct { name string; age int }

package main

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

func main() {
    text := `Name: {{ .Name }}
Age: {{ .Age }}
`

    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    // field名は大文字で始める
    v := struct {
        Name string
        Age  int
    }{
        Name: "Tanaka",
        Age:  32,
    }

    if err := tpl.Execute(os.Stdout, v); err != nil {
        log.Fatal(err)
    }
    // 出力
    // Name: Tanaka
    // Age: 31
}



値の直前/直後の空白や改行を取り除く


{{- .B -}} のように - で直前、直後の空白や改行を全て消すことができる。
直前だけであれば {{- .B }} 直後だけなら {{ .B -}} とする。
if文なんかで、インデントをつけて見やすくしたときに {{- .B }} とすることで余計な空白を除去できる。

package main

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

func main() {
    text := "A  {{- .B -}}  C  {{- .D -}}  E\n"

    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    m := map[string]interface{}{
        "B": "B",
        "D": "D",
    }

    if err := tpl.Execute(os.Stdout, m); err != nil {
        log.Fatal(err)
    }
    // ABCDE
    //
    // - を使用しなければ空白はそのまま
    // A  B  C  D  E
    //
}



変数


変数名は $ で始まりアルファベット大文字/小文字、数値、アンダーバーが使用可能
$ から始まっていればいいので $1$_ でもよい。

text := `
変数の定義   {{ $v := 100 }}
変数の使い方 {{ $v }}
`



パイプ


| を使って値を受け渡す

`
{{ "put" | printf "%s%s" "out" }}
`

// 上は以下と同義

`
{{ printf "%s%s" "out" "put" }}
`



with


with は、値が空(型の初期値)ではない場合に、その値を . として
{{ with }} ... {{ end }} のブロック内で使うことができる。
値が空の場合は if 文と同じでそのブロックは実行されない。

`
{{ with .value }}
  {{ . }}
{{ end }}

{{ with "hello" }}
  {{ . }}
{{ end }}
`


変数に代入して使うこともできる

`
{{ with $v := .value }}
  {{ $v }}
{{ end }}

{{ with $v := "hello" }}
  {{ $v }}
{{ end }}
`


if 文のように else を使うこともできて、以下の.value が空だった場合に実行される

`
{{ with .value}}
  {{ . }}
{{ else }}
  value is empty!
{{ end }}

`



if文


論理演算子と比較演算子は関数として定義されている

論理演算子

and  &&
not  !
or   || 

比較演算子

eq   ==
ne   !=
lt   <
le   <=
gt   >
ge   >=


with と同じで空の値は偽とみなされるので実行されない。
with と違ってブロック内で . に値はセットされない

`
{{ $x := 0 }}
{{ if $x }}
    {{ $x }}
{{ end }}
`


演算子は関数なので、 eq .x 100 のように引数として渡す。
以下の例は if x == 100 と同義

`
{{ if eq .x 100 }}
    {{ .x }}
{{ end }}
`


andeq と同じで関数なので引数として渡す感じ。
以下は if x == 10 && y == 20 と同義

`
{{ if and (eq .x 10) (eq .y 20) }}
    x: {{ .x }}, y {{ .y }}
{{ end }}
`


勿論 else ifelse も使える

`
{{ if eq .x 10 }}
  {{ .x }} is 10
{{ else if  eq .x 20 }}
  {{ .x }} is 20
{{ else }}
  {{ .x }}
{{ end }}
`



range


スライスの値は . に入ってる

`
{{ range .slice }}
  {{- . }}
{{ end }}
`


インデックス番号や値を変数で使う

`
{{ range $i, $v := .slice }}
index: {{ $i }}, value: {{ $v }}
{{ end }}
`


マップ。通常の for range と同じでマップのキーと値が変数に入っている

`
{{ range $key, $value := .map }}
key: {{ $key }}, value: {{ $value }}
{{ end }}
`


スライスやマップが空だったときに実行する処理を else を使ってかける

`
{{ range .slice }}
  {{- . }}
{{ else }}
.slice is empty!
{{ end }}
`



ビルトイン関数


ビルトイン関数は以下のように定義されている。
https://golang.org/src/text/template/funcs.go#L32

var builtins = FuncMap{
    "and":      and,
    "call":     call,
    "html":     HTMLEscaper,
    "index":    index,
    "slice":    slice,
    "js":       JSEscaper,
    "len":      length,
    "not":      not,
    "or":       or,
    "print":    fmt.Sprint,
    "printf":   fmt.Sprintf,
    "println":  fmt.Sprintln,
    "urlquery": URLQueryEscaper,

    // Comparisons
    "eq": eq, // ==
    "ge": ge, // >=
    "gt": gt, // >
    "le": le, // <=
    "lt": lt, // <
    "ne": ne, // !=
}

説明については https://golang.org/pkg/text/template/ の Functions に載ってます。


index インデクシング

// A[4] 四番目の値を取り出す
`
{{ index .A 4 }}
`


slice スライシング

// A[1:] スライスの 1つから最後まで
`
{{ slice .A 1 }}
`

// A[2:3] 2番目から3番目まで
`
{{ slice .A 2 3 }}
`

// A[1:3:3] 最後の :N はキャパシティの指定
`
{{ slice . 1 3 3 }}
`



独自の関数の追加


以下の例では createElement という関数を追加してテンプレート内で使ってます。
HTMLを返す関数なのでエスケープしたい場合は text/template ではなく、
html/template を使うと自動でエスケープしてくれる。

package main

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

func main() {
    text := `
{{- createElement "div" "hello world" }}
`

    funcmap := template.FuncMap{
        "createElement": func(tagname, text string) string {
            return fmt.Sprintf("<%s>%s</%s>", tagname, text, tagname)
        },
    }

    tpl := template.New("")

    tpl = tpl.Funcs(funcmap)

    var err error
    tpl, err = tpl.Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    if err := tpl.Execute(os.Stdout, nil); err != nil {
        log.Fatal(err)
    }
    //
    // 出力
    //
    // <div>hello world</div>
    //
    // html/template を使うと以下のようにエスケープされる
    //
    // &lt;div&gt;hello world&lt;/div&gt;
    //
    //
    // html/template を使ってエスケープしないようにするには、たぶんこうする
    //
    // func(tagname, text string) template.HTML {
    //     return template.HTML(fmt.Sprintf("<%s>%s</%s>", tagname, text, tagname))
    // }
    //
}


当然、構造体やマップに関数を定義して渡すことでもできる

package main

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

type Profile struct {
    Name string
    Age  int
}

func (p Profile) ToString() string {
    return fmt.Sprintf("Name: %s, Age: %d", p.Name, p.Age)
}

func main() {
    text := `
{{- .ToString }}
`
    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    p := &Profile{"Tanaka", 31}

    if err := tpl.Execute(os.Stdout, p); err != nil {
        log.Fatal(err)
    }
}



define, block, template


{{ define "name" }} でテンプレート名を定義して、{{ template "name" }} で呼び出す。
呼び出した側で何らかの処理を行いたい場合は {{ template "name" . }}.
呼び出したテンプレートが入っているので、パイプ処理できる。

template.ParseFiles 関数でファイルを読み込む場合は先頭のファイルがメタファイルとみなされる。
順不同で読み込んだり template.ParseGlob 関数を使う場合は集約するファイルにもテンプレート名を書いておく必要がある。
その場合は、tpl.ExecuteTemplate(os.Stdout, "name", nil) のように第二引数で指定する。

{{ define "name" }} とその呼び出しの {{ template "name" }} は一つのファイル内に書いても同じ。
ただし、 {{ define "name" }} ... {{ end }} の中で define を使うとエラーになる。
template: index.html:: unexpected <define> in command
{{ block "name" . }} は、 {{ define "name" }}その場実行版でネストしてもエラーにはならない


package main

import (
    "log"
    "os"
    "html/template"
)

func main() {
    text := `
{{- define "contents" }}
<!-- ネスト -->
{{ block "header" . }}
<div>header</div>
{{ end }}
<div>contents</div>
{{ end }}

{{- define "footer" }}
<div>footer</div>
{{ end -}}

{{- block "index" . -}}
<!DOCTYPE html>
<html lang="ja">
<head></head>
<body>
  {{ template "contents" . }}
  {{ template "footer" . }}
</body>
</html>
{{ end -}}
`

    tpl, err := template.New("").Parse(text)
    if err != nil {
        log.Fatal(err)
    }

    if err := tpl.Execute(os.Stdout, nil); err != nil {
        log.Fatal(err)
    }
}


出力

<!DOCTYPE html>
<html lang="ja">
<head></head>
<body>
  
<!-- ネスト -->

<div>header</div>

<div>contents</div>

<div>footer</div>

</body>
</html>



テンプレートファイルの読み込み


 .
 ├── contents.html
 ├── footer.html
 ├── header.html
 ├── index.html
 └── main.go

メタファイルになる index.html から header, contents, footer を呼び出す。


  • index.html
{{ block "index" . }}
<!DOCTYPE html>
<html lang="ja">
<head></head>
<body>
  {{ template "header" }}
  {{ template "contents" }}
  {{ template "footer" }}
</body>
</html>
{{ end }}


  • header.html
{{ define "header" }}
  <div>header</div>
{{ end }}


  • contents.html
{{ define "contents" }}
  <div>contents</div>
{{ end }}


  • footer.html
{{ define "footer" }}
  <div>footer</div>
{{ end }}


  • main.go
package main

import (
    "log"
    "os"
    "html/template"
)

func main() {
    tpl := template.Must(template.ParseGlob("*.html"))

    if err := tpl.ExecuteTemplate(os.Stdout, "index", nil); err != nil {
        log.Fatal(err)
    }

    // ParseFiles で、引数の先頭を index.html にしておけば {{ block "index" . }} を書く必要がなくなる
    // tpl, err := template.ParseFiles("index.html", "header.html", "contents.html", "footer.html")
    // その場合は、Execute を使う
    // tpl.Execute(os.Stdout, nil)
}


出力

<!DOCTYPE html>
<html lang="ja">
<head></head>
<body>
  <div>header</div>
  <div>contents</div>
  <div>footer</div>
</body>
</html>