【追記】もうちょっと例が欲しくなったので, その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)の内容をエスケープする
エスケープ用のメソッドが用意されています.
テンプレート用の関数にもデフォルトでhtmlなどのメソッドがあります.
とはいえ, 特別な事情がない限り, デフォルトでエスケープされるhtml/templateを利用するべきでしょう.