write ahead log

ロールフォワード用

golangでテンプレートエンジンを使う

【追記】もうちょっと例が欲しくなったので, その2も書きました


golangではテンプレートエンジンが標準で用意されています.

なんだけど, ちょっと癖があって僕はすぐにはなじめなかった.

まぁ, テンプレートエンジンなんて慣れの問題な気がするけど.

調べながら書いたらめちゃ長くなった.

ざっくり概要

golangのテンプレートエンジンはtext/templateパッケージに入っている.

Web開発では同じインターフェースを持ったhtml/templateパッケージを利用することになる.(デフォルトでエスケープ処理が行われる)

このパッケージのメソッドに, UTF-8のテキストとデータを渡せばいい感じにテキストを生成してくれる.

以下は基本的にtext/templateを利用した説明になるけど, html/templateでもそのまま動くはず.(エスケープされた結果で)

一番簡単な例

一番簡単なのはパラメータもない単純なテキストからテンプレートを作る例でしょう.

package main

import (
    "text/template"
    "os"
)

func main() {
    // テンプレート名:"template name"で"Hello, text/template"という内容のテンプレートオブジェクトを生成
    tmpl, err := template.New("template name").Parse("Hello, text/template")
    if err != nil {
        panic(err)
    }
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err = tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果]

$ ./template.exe
Hello, text/template

ファイルからテンプレートのテキストを読みだす

いちいちParseに文字列渡すなんて面倒でしょうがないですから, 当然ファイルから読みだしてParseするメソッドがあります.

package main

import (
    "text/template"
    "os"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    tmpl, err := template.ParseFiles("test.tmpl")
    if err != nil {
        panic(err)
    }
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err = tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(test.tmpl)]

Hello, text/template.

[実行結果]

$ ./template.exe
Hello, text/template

テンプレートオブジェクトの名前を調べる

ところで上の例ではテンプレートオブジェクトの名前を指定していません.

名前は何になっているんですかね.

Nameというメソッドがあるので, これを使って調べてみます.

package main

import (
    "text/template"
    "fmt"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    tmpl, err := template.ParseFiles("test.tmpl")
    if err != nil {
        panic(err)
    }
    // 名前を出力
    fmt.Println(tmpl.Name())
}

[実行結果]

$ ./template.exe
test.tmpl

なるほど, ファイル名になっているようです.

Parse時にエラー処理を行う

上記までの例ではParseした後, いちいちエラー処理を書いていますが, 面倒です.

そもそも大半のアプリでは初期化時で1度Parseすれば十分でしょう.
(ファイルが無くてpanicとか起動時に知りたいですし)

そのためにMustというメソッドが用意されています.

これはエラーが起きたらpanic起こす, という単純なメソッドです.

package main

import (
    "text/template"
    "os"
)

func main() {
    // "test.tmpl"というファイルの内容からテンプレートオブジェクトを生成
    // ファイルが無かったりして失敗したらpanic起こす
    tmpl := template.Must(template.ParseFiles("test.tmpl"))
    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果(ファイルあり)]

$ ./template.exe
Hello, text/template.

[実行結果(ファイルなし)]

$ ./template.exe
panic: open test.tmpl: The system cannot find the file specified.

goroutine 1 [running]:
panic(0x510b60, 0xc042056210)
        C:/Go/src/runtime/panic.go:500 +0x1af
text/template.Must(0x0, 0x5b8400, 0xc042056210, 0x0)
        C:/Go/src/text/template/helper.go:23 +0x6d
main.main()
        C:/msys64/home/twinbird/dropbox/lab/template/sample.go:10 +0x76

これで少し楽できます.

複数のテンプレートを扱う

templateパッケージでは複数のテンプレートを1つのテンプレートオブジェクトで取り扱うことができます.

下記の例ではParseFilesで3つのテンプレートファイルを展開しておき, ExecuteTemplateでテンプレートを選択して出力しています.

package main

import (
    "text/template"
    "os"
)

func main() {
    // tmplディレクトリの下の3つのファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("tmpl/test.tmpl", "tmpl/test2.tmpl", "tmpl/test3.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力

    // Template1
    err := tmpl.ExecuteTemplate(os.Stdout, "test.tmpl", nil)
    if err != nil {
        panic(err)
    }
    // Template2
    err = tmpl.ExecuteTemplate(os.Stdout, "test2.tmpl", nil)
    if err != nil {
        panic(err)
    }
    // Template3
    err = tmpl.ExecuteTemplate(os.Stdout, "test3.tmpl", nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(test.tmpl)]

Template1

[テンプレート(test2.tmpl)]

Template2

[テンプレート(test3.tmpl)]

Template3

[実行結果]

$ ./template.exe
Template1
Template2
Template3

複数のテンプレートを扱う(もう少し)

ここまで来ると使う機会なさそうな機能ですけど.

package main

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

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseGlob("tmpl/*.tmpl"))

    // Debug用にテンプレート一覧を出力
    fmt.Println(tmpl.DefinedTemplates())

    // templateのスライスを取得してテンプレート数を出力してみる
    fmt.Println(len(tmpl.Templates()))

    // test.tmplのオブジェクトを取得
    tmpl1 := tmpl.Lookup("test.tmpl")

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl1.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[実行結果]

$ ./template.exe
; defined templates are: "test.tmpl", "test2.tmpl", "test3.tmpl"
3
Template1

引数(パラメータ)を渡す

さて, 実際テンプレートエンジンを使う際, パラメータを埋め込めなければ意味なんてあまりありません.

templateでは {{と}}で囲んだ中身が「アクション」と呼ばれます.

アクションの中では「式」や「パイプライン」を使ってデータや式を評価することができます.
(そしてこれが個人的には曲者です)

一番簡単な例

以下の例ではGreeting構造体をテンプレートに渡して, テンプレート内では.(ドット)で参照して表示に使っています.

package main

import (
    "os"
    "text/template"
)

// テンプレートへ引き渡すオブジェクト
// メンバはエクスポート(大文字)しておかないと参照できない
type Greeting struct {
    Messege string
    Target  string
}

func main() {
    // テンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // テンプレートへ渡すデータを作る
    g := &Greeting{
        "hello",
        "world",
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, g) // <- 第二引数にデータを渡す
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Messege}}, {{.Target}}.

[実行結果]

$ ./template.exe
Greeting

hello, world.
複数データ(スライス)を扱う

複数のデータを扱えないとさすがに困る.

rangeというアクションがあるのでこれを使うと繰り返しが書けます.

ちょい戸惑ったのがrangeの中では.(ドット)の意味が変わるということ.

range~endのループの間では.(ドット)に各要素の値がセットされます.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{end}}

[実行結果]

$ ./template.exe
Greeting


hello, world.

こんにちは, 世界.

Hallo, Welt.

你好, 世界.
テンプレートを内で分岐する

あまりやりたいことではないですが, テンプレート内でif文を使って分岐したい事がありますね.

templateパッケージで分岐する際にはifアクションを利用します.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
        false,
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
        true,
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
        false,
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
        true,
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{if .Print}}
{{.Messege}}, {{.Target}}.
{{end}}
{{end}}

[実行結果]

$ ./template.exe
Greeting





こんにちは, 世界.





你好, 世界.

else文, else if文も以下の感じで普通に使えます.

Greeting

{{range .}}
{{if .Print}}
{{.Messege}}, {{.Target}}.
{{else}}
NO PRINT.
{{end}}
{{end}}
rangeを使ってデータの有無で分岐する

rangeを使うとデータセットの有無で分岐を行うことができます. ちなみにrangeに渡せるのは以下の通りです.

  • 配列
  • スライス
  • マップ
  • チャネル
package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // 空のスライス準備
    messeges := make([]*Greeting, 0)

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{else}}
NO MESSEGE.
{{end}}

[実行結果]

$ ./template.exe
Greeting


NO MESSEGE.
テンプレート内から別のテンプレートを呼び出す

テンプレートにテンプレートを埋め込む事ができます.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl", "inner_hello.tmpl"))

    // 空のスライス準備
    messeges := make([]*Greeting, 0)

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[親のテンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{.Messege}}, {{.Target}}.
{{else}}
{{template "inner_hello.tmpl"}}
{{end}}

[子のテンプレート(inner_hello.tmpl)]

Hello, world from inner template.

[実行結果]

$ ./template.exe
Greeting


Hello, world from inner template.
埋め込んだテンプレートに引数を渡す

埋め込んだテンプレートに対して引数を渡せないと, 利点は薄いですね.

少し冗長というか, 無駄っぽい例ですが.

package main

import (
    "os"
    "text/template"
)

type Greeting struct {
    Messege string
    Target  string
    Print   bool // 印字するかのフラグ
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl", "inner_hello.tmpl"))

    // スライス準備
    messeges := make([]*Greeting, 4)

    // テンプレートへ渡すデータ
    messeges[0] = &Greeting{
        "hello",
        "world",
        false,
    }
    messeges[1] = &Greeting{
        "こんにちは",
        "世界",
        true,
    }
    messeges[2] = &Greeting{
        "Hallo",
        "Welt",
        false,
    }
    messeges[3] = &Greeting{
        "你好",
        "世界",
        true,
    }

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, messeges)
    if err != nil {
        panic(err)
    }
}

[親テンプレート(hello_world.tmpl)]

Greeting

{{range .}}
{{template "inner_hello.tmpl" .}}
{{end}}

[子テンプレート(inner_hello.tmpl)]

{{.Messege}} {{.Target}} from inner template.
$ ./template.exe
Greeting


hello world from inner template.


こんにちは 世界 from inner template.


Hallo Welt from inner template.


你好 世界 from inner template.
変数を扱う

滅多にない事な気がしますが, テンプレート内で変数を扱う事ができます.

変数は$(ドルマーク)を先頭につけて宣言, 利用します.

package main

import (
    "os"
    "text/template"
)

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{$variable := "変数だよ"}}

{{$variable}}

[実行結果]

$ ./template.exe
Greeting



変数だよ

ちなみにrangeアクションは普通のgoプログラムと同じで, indexと要素それぞれを返すことができます.

[例]

{{ /* .にはスライスとかが入ってる感じ */ }}
{{range $index, $element := . }}
コメント

コメントは普通に

/* something */

の形式です.

メソッド(引数なし)

メソッドはテンプレート呼び出し元のgoプログラムで書かれたものをそのまま利用できます.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    Value string
}

func (obj *Object)Method() string {
    return obj.Value
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{"Hello, world from method"}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Method}}

[実行結果]

$ ./template.exe
Greeting

Hello, world from method
メソッド(引数あり)

テンプレートから引数付きでメソッドを呼び出すときにはスペース区切りでパラメータを渡します.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    Value string
}

func (obj *Object)Method(str1 string, str2 string) string {
    return obj.Value + " " + str1 + " " + str2
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{"Hello, world from method"}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{.Method "param1" "param2"}}

[実行結果]

$ ./template.exe
Greeting

Hello, world from method param1 param2
関数を使う

テンプレート内での関数はgoプログラムの関数の名前空間とは異なるものになっています.

デフォルトではいくつかの関数がグローバル空間に定義されていて, それ以上の独自のものは自分で登録する必要があります.
(call関数やメソッドで何とかする手もあります.が, それが面倒な状況もあるでしょう)

ややこしいcallだけ例を書いておきます.

特に以下は注意が必要でしょう.

最初の引数は関数型の値(printのような組み込み関数ではないもの)を生成する評価結果でなくてはなりません。 関数は1つまたは2つの値を返す必要があり、2番目はerror型になります。 引数が関数型でない、あるいは戻り値のerror値がnilでなかった場合、実行は停止します。

要するに関数は

func() string

func() (string, error)

である必要があるということでしょう.
(もちろんstringは任意)

2つ目の例ではエラーがnilでなければ停止ということですね.

package main

import (
    "os"
    "text/template"
)

type Object struct {
    SomethingToDo func(str1 string, str2 string) (string, error)
}

func SampleFunction(str1 string, str2 string) (string, error) {
    return "This is sample." + " " + str1 + " " + str2, nil
}

func main() {
    // tmplディレクトリの下のファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("hello_world.tmpl"))

    obj := &Object{SampleFunction}

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, obj)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

{{call .SomethingToDo "param1" "param2"}}

[実行結果]

$ ./template.exe
Greeting

This is sample. param1 param2
独自関数を作る

テンプレート用に独自関数を用意するにはFuncsメソッドを使います.

Parse前に設定してやらないとダメみたいです.(そりゃそうか)

ハマったのは新規でテンプレートを作成して, そこに関数を定義しましたが, ParseFilesを使うと ファイル名がテンプレート名になるので, 関数が未定義というpanicが発生しました.

うまい方法があるのかもしれませんが, この辺り良いやり方がわかってないです.

package main

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

func numberToDollar(v float64) string {
    return fmt.Sprintf("$%f", v)
}

func numberToYen(v float64) string {
    return fmt.Sprintf("¥%f", v)
}

func main() {
    // テンプレート用の関数マップを作成
    // 関数名: 関数オブジェクトの形式
    funcMap := template.FuncMap{
        "ToDollar": numberToDollar,
        "ToYen": numberToYen,
    }
    // テンプレート作成(ParseFiles使うときは名前をParseFilesに使うものと同じにしないとpanicになる)
    tmpl := template.New("hello_world.tmpl")
    // 関数定義
    tmpl = tmpl.Funcs(funcMap)
    // ファイルの内容からテンプレートオブジェクトを生成
    tmpl = template.Must(tmpl.ParseFiles("hello_world.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(hello_world.tmpl)]

Greeting

Dollar: {{ToDollar 123.456}}
Yen: {{ToYen 123.456}}

[実行結果]

$ ./template.exe
Greeting

Dollar: $123.456000
Yen: ¥123.456000
テンプレート内でのテンプレート定義(defineアクション)

テンプレート内でテンプレートを定義することができます. (defineアクションを利用)

用途があまり思いつかない....

Web開発とかでは部分ごとにテンプレートを分けて, templateアクションでまとめるのが王道っぽい気がします.

package main

import (
    "os"
    "text/template"
)

func main() {
    // ファイルの内容からテンプレートオブジェクトを生成
    tmpl := template.Must(template.ParseFiles("page.tmpl"))

    // テンプレートからテキストを生成して, os.Stdoutへ出力
    err := tmpl.Execute(os.Stdout, nil)
    if err != nil {
        panic(err)
    }
}

[テンプレート(page.tmpl)]

{{define "header"}}
Header

This is a header text.
{{end}}

{{define "body"}}
Body

This is a body text.
{{end}}

{{define "footer"}}
Footer

This is a footer text.
{{end}}

{{define "page"}}
{{template "header"}}
{{template "body"}}
{{template "footer"}}
{{end}}

{{template "page"}}

[実行結果]










Header

This is a header text.


Body

This is a body text.


Footer

This is a footer text.
パイプライン

上述までのアクションで使う変数は実はパイプラインと呼ばれるそうです.
(また新しい単語が...)

パイプラインはUnixのシェルと同じ様な考え方をします.

シェルのパイプと同様に|で変数や関数を繋いでいくわけです.

パイプで繋がれた先の関数では第一引数に繋ぎ元の結果が渡されます.

第二引数でerrがnilでなければpanicです.

[例1]

単一項目のパイプライン

{{10.00}}

[実行結果1]

10

[例2]

チェインしたパイプライン

{{10.00 | printf %.2f}}

[実行結果2]

10.00

Web開発のためにHTML(とJS)の内容をエスケープする

エスケープ用のメソッドが用意されています.

HTMLEscape - golang.org

テンプレート用の関数にもデフォルトでhtmlなどのメソッドがあります.

とはいえ, 特別な事情がない限り, デフォルトでエスケープされるhtml/templateを利用するべきでしょう.