GolangのテンプレートによるHTML/テキストの動的な生成
はじめに
Golangには、テンプレートを使用して動的にテキストやHTMLを生成するための標準ライブラリが提供されています。text/templateとhtml/templateの2つのパッケージがあり、これらを使うことで効率的に文字列を組み立てることができます。
この記事では、これらのテンプレートライブラリの基本的な使い方を解説します。
テンプレートパッケージの概要
Golangには次の2つのテンプレートパッケージがあります:
- text/template: 一般的なテキスト出力用のテンプレートエンジン
- html/template: HTMLやその他のコンテンツタイプを安全に出力するためのテンプレートエンジン(XSS攻撃対策が組み込まれている)
html/templateはtext/templateのラッパーであり、同じAPIを持ちますが、出力時にHTMLエスケープを自動的に行うため、Webアプリケーションで使用する場合はhtml/templateが推奨されます。
基本的な使い方
テンプレートの作成と実行
簡単な例から見ていきましょう:
package main
import (
"os"
"text/template"
)
func main() {
// テンプレート文字列の定義
tmpl := "こんにちは、{{.Name}}さん!\n"
// テンプレートの解析
t, err := template.New("greeting").Parse(tmpl)
if err != nil {
panic(err)
}
// データの定義
data := struct {
Name string
}{
Name: "ゴファー",
}
// テンプレートの実行と出力
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
この例では:
- テンプレート文字列を定義
template.New()でテンプレートを作成し、Parse()で解析- テンプレートに渡すデータを構造体で定義
Execute()メソッドでテンプレートを実行し、結果を出力
実行すると、「こんにちは、ゴファーさん!」と出力されます。
ファイルからテンプレートを読み込む
実際のアプリケーションでは、テンプレートを外部ファイルとして管理することが一般的です:
package main
import (
"os"
"text/template"
)
func main() {
// ファイルからテンプレートを解析
t, err := template.ParseFiles("templates/greeting.tmpl")
if err != nil {
panic(err)
}
data := struct {
Name string
}{
Name: "ゴファー",
}
// テンプレートの実行
err = t.Execute(os.Stdout, data)
if err != nil {
panic(err)
}
}
templates/greeting.tmplの内容:
こんにちは、{{.Name}}さん!
HTMLテンプレートの例
html/templateパッケージを使用した例:
package main
import (
"html/template"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
// テンプレートの解析
t, err := template.ParseFiles("templates/index.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// データの定義
data := struct {
Title string
Message string
}{
Title: "Goのテンプレート",
Message: "<script>alert('XSS');</script>",
}
// テンプレートの実行
err = t.Execute(w, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
templates/index.htmlの内容:
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
</body>
</html>
この例では、Messageに含まれるJavaScriptコードは自動的にエスケープされ、安全に出力されます。
テンプレート構文
変数とフィールドの参照
{{.}}- 現在のデータ(ドット)を参照{{.Field}}- 現在のデータのフィールドを参照{{.Method}}- 現在のデータのメソッドを呼び出し
条件分岐
{{if .Condition}} 真の場合の処理 {{else}} 偽の場合の処理 {{end}}
または
{{if .Condition1}}
条件1が真の場合の処理
{{else if .Condition2}}
条件2が真の場合の処理
{{else}}
どの条件も満たさない場合の処理
{{end}}
ループ処理
{{range .Items}}
{{.}} <!-- 現在の要素 -->
{{end}}
インデックスと値を取得する場合:
{{range $index, $element := .Items}}
{{$index}}: {{$element}}
{{end}}
変数の定義
{{$variable := .Field}}
{{$variable := functionCall}}
テンプレート内の関数
テンプレート内では以下のような組み込み関数が使用できます:
len- 長さを取得eq,ne,lt,le,gt,ge- 比較演算and,or,not- 論理演算print,printf,println- 出力関数html,js,urlquery- エスケープ関数
例:
{{if eq .Count 1}}単数{{else}}複数{{end}}
カスタム関数の登録
独自の関数をテンプレートに登録することもできます:
funcMap := template.FuncMap{
"add": func(a, b int) int {
return a + b
},
"upper": strings.ToUpper,
}
t := template.New("example").Funcs(funcMap)
t, err := t.Parse("{{.Name | upper}}, {{add 1 2}}")
テンプレート内での使用:
{{.Name | upper}} <!-- 大文字に変換 -->
{{add 1 2}} <!-- 3を出力 -->
パイプライン
複数の操作を連結できます:
{{.Message | html | printf "<div>%s</div>"}}
テンプレートの入れ子と再利用
テンプレートの定義と使用
<!-- ベーステンプレート -->
{{define "base"}}
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
<header>ヘッダー</header>
{{template "content" .}}
<footer>フッター</footer>
</body>
</html>
{{end}}
<!-- コンテンツテンプレート -->
{{define "content"}}
<h1>{{.Title}}</h1>
<p>{{.Message}}</p>
{{end}}
使用例:
t, err := template.ParseFiles("templates/base.tmpl", "templates/content.tmpl")
err = t.ExecuteTemplate(w, "base", data)
実践的な例
フォームデータの表示
package main
import (
"html/template"
"net/http"
)
type User struct {
Name string
Email string
Age int
}
func formHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "POST" {
// フォームデータの解析
err := r.ParseForm()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// ユーザーデータの作成
user := User{
Name: r.Form.Get("name"),
Email: r.Form.Get("email"),
Age: 20, // 固定値
}
// テンプレートの解析と実行
t, err := template.ParseFiles("templates/result.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, user)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
} else {
// GETリクエストの場合はフォームを表示
t, err := template.ParseFiles("templates/form.html")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
}
func main() {
http.HandleFunc("/", formHandler)
http.ListenAndServe(":8080", nil)
}
templates/form.html:
<!DOCTYPE html>
<html>
<head>
<title>ユーザーフォーム</title>
</head>
<body>
<h1>ユーザー情報を入力してください</h1>
<form method="post">
<div>
<label for="name">名前:</label>
<input type="text" id="name" name="name">
</div>
<div>
<label for="email">メール:</label>
<input type="email" id="email" name="email">
</div>
<button type="submit">送信</button>
</form>
</body>
</html>
templates/result.html:
<!DOCTYPE html>
<html>
<head>
<title>登録完了</title>
</head>
<body>
<h1>登録完了</h1>
<p>こんにちは、{{.Name}}さん!</p>
<p>メールアドレス: {{.Email}}</p>
<p>年齢: {{.Age}}</p>
</body>
</html>
エラーハンドリング
Goのテンプレートを使用する際には、適切なエラーハンドリングが重要です。テンプレート処理中に発生する可能性のあるエラーとその対処法について説明します。
一般的なエラーパターン
テンプレート処理で発生しやすい主なエラーパターンは以下の通りです:
- テンプレート構文エラー:テンプレートの解析時に発生します
- 実行時エラー:テンプレートの実行時に発生します
- 存在しないフィールドや関数の参照:データ構造にないフィールドを参照した場合に発生します
- 型の不一致:期待される型と異なる型のデータが渡された場合に発生します
構文エラーの処理
テンプレートの構文エラーは、Parse()メソッドの呼び出し時に検出されます:
tmpl := "こんにちは、{{.Name}さん!" // 閉じ括弧が不足
t, err := template.New("greeting").Parse(tmpl)
if err != nil {
// エラーハンドリング
fmt.Printf("テンプレート解析エラー: %v\n", err)
return
}
このようなエラーは開発中に検出して修正すべきものです。
実行時エラーの処理
テンプレートの実行時にもエラーが発生する可能性があります:
data := struct {
// Name フィールドが欠落している
}{}
err = t.Execute(os.Stdout, data)
if err != nil {
// エラーハンドリング
fmt.Printf("テンプレート実行エラー: %v\n", err)
return
}
エラー情報を表示するカスタムテンプレート
開発環境では、詳細なエラー情報をユーザーに表示することが役立つ場合があります:
func errorHandler(w http.ResponseWriter, status int, err error) {
w.WriteHeader(status)
errorTmpl := `
<html>
<head><title>エラー</title></head>
<body>
<h1>エラーが発生しました</h1>
<p>{{.}}</p>
</body>
</html>
`
t, parseErr := template.New("error").Parse(errorTmpl)
if parseErr != nil {
// テンプレート自体にエラーがある場合は単純なエラーメッセージを返す
http.Error(w, err.Error(), status)
return
}
t.Execute(w, err.Error())
}
ゼロ値の処理
テンプレート内でフィールドが存在しない場合、Goはゼロ値を返します。これを利用して条件分岐で対応することができます:
{{if .OptionalField}}
{{.OptionalField}}を使用します
{{else}}
OptionalFieldは設定されていません
{{end}}
エラー発生時の代替コンテンツの表示
エラーが発生した場合に代替コンテンツを表示する方法:
func renderTemplate(w http.ResponseWriter, templateName string, data interface{}) {
t, err := template.ParseFiles("templates/" + templateName + ".html")
if err != nil {
// テンプレートが見つからない場合はフォールバックテンプレートを使用
fallbackTmpl := `<html><body><h1>コンテンツは現在利用できません</h1></body></html>`
w.Write([]byte(fallbackTmpl))
// エラーをログに記録
log.Printf("テンプレートエラー: %v", err)
return
}
err = t.Execute(w, data)
if err != nil {
// 実行エラーの場合も同様に処理
w.Write([]byte("エラーが発生しました"))
log.Printf("テンプレート実行エラー: %v", err)
}
}
パフォーマンスのためのキャッシング
テンプレートの解析は比較的コストの高い操作です。パフォーマンスを向上させるために、テンプレートをキャッシュすることをお勧めします:
var templates = template.Must(template.ParseGlob("templates/*.html"))
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
err := templates.ExecuteTemplate(w, name, data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
ここでtemplate.Must関数は、エラーが発生した場合にパニックを発生させるラッパーです。これは通常、アプリケーションの初期化時にのみ使用すべきです。
エラーロギング
実運用環境では、詳細なエラー情報をユーザーに表示するのではなく、ログに記録することが重要です:
func renderTemplate(w http.ResponseWriter, name string, data interface{}) {
err := templates.ExecuteTemplate(w, name, data)
if err != nil {
// エラーをログに記録
log.Printf("テンプレート %s の実行エラー: %v", name, err)
// ユーザーにはシンプルなエラーメッセージを表示
http.Error(w, "内部サーバーエラー", http.StatusInternalServerError)
}
}
まとめ
Golangのtext/templateとhtml/templateパッケージは、動的なテキストやHTMLを生成するための強力なツールです。基本的な使い方は次のとおりです:
- テンプレートを作成(文字列または外部ファイルから)
- テンプレートを解析
- データを準備
- テンプレートを実行して結果を出力
Webアプリケーションの開発では、セキュリティ上の理由からhtml/templateパッケージを使用することをお勧めします。これにより、XSS攻撃などのセキュリティリスクを軽減できます。
テンプレートの条件分岐、ループ、変数定義などの機能を使用することで、複雑な出力も簡潔に記述できます。また、テンプレートの入れ子や再利用の機能を活用することで、メンテナンス性の高いコードを実現できます。