GolangのテンプレートによるHTML/テキストの動的な生成

はじめに

Golangには、テンプレートを使用して動的にテキストやHTMLを生成するための標準ライブラリが提供されています。text/templatehtml/templateの2つのパッケージがあり、これらを使うことで効率的に文字列を組み立てることができます。

この記事では、これらのテンプレートライブラリの基本的な使い方を解説します。

テンプレートパッケージの概要

Golangには次の2つのテンプレートパッケージがあります:

  • text/template: 一般的なテキスト出力用のテンプレートエンジン
  • html/template: HTMLやその他のコンテンツタイプを安全に出力するためのテンプレートエンジン(XSS攻撃対策が組み込まれている)

html/templatetext/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)
	}
}

この例では:

  1. テンプレート文字列を定義
  2. template.New()でテンプレートを作成し、Parse()で解析
  3. テンプレートに渡すデータを構造体で定義
  4. 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のテンプレートを使用する際には、適切なエラーハンドリングが重要です。テンプレート処理中に発生する可能性のあるエラーとその対処法について説明します。

一般的なエラーパターン

テンプレート処理で発生しやすい主なエラーパターンは以下の通りです:

  1. テンプレート構文エラー:テンプレートの解析時に発生します
  2. 実行時エラー:テンプレートの実行時に発生します
  3. 存在しないフィールドや関数の参照:データ構造にないフィールドを参照した場合に発生します
  4. 型の不一致:期待される型と異なる型のデータが渡された場合に発生します

構文エラーの処理

テンプレートの構文エラーは、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/templatehtml/templateパッケージは、動的なテキストやHTMLを生成するための強力なツールです。基本的な使い方は次のとおりです:

  1. テンプレートを作成(文字列または外部ファイルから)
  2. テンプレートを解析
  3. データを準備
  4. テンプレートを実行して結果を出力

Webアプリケーションの開発では、セキュリティ上の理由からhtml/templateパッケージを使用することをお勧めします。これにより、XSS攻撃などのセキュリティリスクを軽減できます。

テンプレートの条件分岐、ループ、変数定義などの機能を使用することで、複雑な出力も簡潔に記述できます。また、テンプレートの入れ子や再利用の機能を活用することで、メンテナンス性の高いコードを実現できます。

参考リンク