【追記】もうちょっと例が欲しくなったので, その2も書きました
golangではテンプレートエンジンが標準で用意されています.
なんだけど, ちょっと癖があって僕はすぐにはなじめなかった.
まぁ, テンプレートエンジンなんて慣れの問題な気がするけど.
調べながら書いたらめちゃ長くなった.
ざっくり概要
golangのテンプレートエンジンはtext/templateパッケージに入っている.
Web開発では同じインターフェースを持ったhtml/templateパッケージを利用することになる.(デフォルトでエスケープ処理が行われる)
このパッケージのメソッドに, UTF-8のテキストとデータを渡せばいい感じにテキストを生成してくれる.
以下は基本的にtext/templateを利用した説明になるけど, html/templateでもそのまま動くはず.(エスケープされた結果で)
一番簡単な例
一番簡単なのはパラメータもない単純なテキストからテンプレートを作る例でしょう.
package main
import (
"text/template"
"os"
)
func main() {
tmpl, err := template.New("template name").Parse("Hello, text/template")
if err != nil {
panic(err)
}
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() {
tmpl, err := template.ParseFiles("test.tmpl")
if err != nil {
panic(err)
}
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() {
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() {
tmpl := template.Must(template.ParseFiles("test.tmpl"))
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 := template.Must(template.ParseFiles("tmpl/test.tmpl", "tmpl/test2.tmpl", "tmpl/test3.tmpl"))
err := tmpl.ExecuteTemplate(os.Stdout, "test.tmpl", nil)
if err != nil {
panic(err)
}
err = tmpl.ExecuteTemplate(os.Stdout, "test2.tmpl", nil)
if err != nil {
panic(err)
}
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 := template.Must(template.ParseGlob("tmpl/*.tmpl"))
fmt.Println(tmpl.DefinedTemplates())
fmt.Println(len(tmpl.Templates()))
tmpl1 := tmpl.Lookup("test.tmpl")
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",
}
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 := 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{
"你好",
"世界",
}
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 := 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,
}
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 := template.Must(template.ParseFiles("hello_world.tmpl"))
messeges := make([]*Greeting, 0)
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 := template.Must(template.ParseFiles("hello_world.tmpl", "inner_hello.tmpl"))
messeges := make([]*Greeting, 0)
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 := 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,
}
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 := template.Must(template.ParseFiles("hello_world.tmpl"))
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 := template.Must(template.ParseFiles("hello_world.tmpl"))
obj := &Object{"Hello, world from method"}
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 := template.Must(template.ParseFiles("hello_world.tmpl"))
obj := &Object{SampleFunction}
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,
}
tmpl := template.New("hello_world.tmpl")
tmpl = tmpl.Funcs(funcMap)
tmpl = template.Must(tmpl.ParseFiles("hello_world.tmpl"))
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"))
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を利用するべきでしょう.