[Goのはじめ方] Part26: テンプレート処理(html/template)

Go

イントロダクション 🎯

Go言語でWebアプリケーションを開発する際、動的にHTMLコンテンツを生成する必要がよくあります。例えば、ユーザー情報やデータベースから取得したデータをHTMLに埋め込んで表示したい場合などです。

このような場合に活躍するのが、Goの標準パッケージ html/template です。このパッケージを使うと、テンプレートファイル(HTMLファイル)にGoのデータを埋め込み、安全かつ効率的にHTMLを生成できます。

特に重要なのは、html/templateコンテキストに応じた自動エスケープ機能を持っている点です。これにより、クロスサイトスクリプティング(XSS)などの脆弱性を防ぎ、安全なWebアプリケーションを構築できます。💪

text/template という似たパッケージもありますが、HTMLを出力する場合は、セキュリティ機能が組み込まれている html/template を使うことが強く推奨されます。

基本的な使い方 🛠️

html/template の基本的な流れは以下のようになります。

  1. テンプレート文字列やテンプレートファイルを準備する。
  2. template.New() でテンプレートオブジェクトを作成し、Parse()ParseFiles(), ParseGlob() でテンプレートを解析する。
  3. テンプレートに埋め込むデータ(構造体やマップなど)を用意する。
  4. Execute() メソッドを使って、データとテンプレートを結合し、HTMLを出力する。

簡単な例

まずは、テンプレート文字列を使った簡単な例を見てみましょう。

package main

import (
	"html/template"
	"os"
)

func main() {
	// テンプレート文字列
	templateString := `<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}}</title>
</head>
<body>
    <h1>{{.Title}}</h1>
    <p>{{.Message}}</p>
</body>
</html>`

	// テンプレートを解析
	tmpl, err := template.New("basic").Parse(templateString)
	if err != nil {
		panic(err)
	}

	// テンプレートに渡すデータ
	data := struct {
		Title   string
		Message string
	}{
		Title:   "テンプレートの基本",
		Message: "こんにちは、Goテンプレート! 👋",
	}

	// テンプレートを実行して標準出力に出力
	err = tmpl.Execute(os.Stdout, data)
	if err != nil {
		panic(err)
	}
}

このコードを実行すると、標準出力に次のようなHTMLが出力されます。

<!DOCTYPE html>
<html>
<head>
    <title>テンプレートの基本</title>
</head>
<body>
    <h1>テンプレートの基本</h1>
    <p>こんにちは、Goテンプレート! 👋</p>
</body>
</html>

{{.Title}}{{.Message}} の部分が、Execute メソッドに渡された構造体のフィールド値に置き換えられています。{{.}} は渡されたデータ全体を参照します。構造体の場合は、{{.フィールド名}} のようにアクセスします。

テンプレートアクション 🎬

テンプレート内では {{ }} で囲まれた部分に「アクション」と呼ばれる特別な命令を書くことができます。これにより、値の埋め込みだけでなく、条件分岐や繰り返し処理なども行えます。

主なアクション

アクション 説明
{{.}} 現在のコンテキストのデータを参照します。 <p>{{.}}</p> (文字列データの場合)
{{.FieldName}} 構造体やマップのフィールド/キーを参照します。 <h1>{{.Title}}</h1>
{{if pipeline}} ... {{else}} ... {{end}} 条件分岐を行います。pipelineが空でない(false, 0, nil, 空文字列/配列/スライス/マップでない)場合に最初のブロックが実行されます。 {{if .IsAdmin}}管理者{{else}}一般ユーザー{{end}}
{{range pipeline}} ... {{else}} ... {{end}} 配列、スライス、マップ、チャネルを反復処理します。. は各要素を参照します。要素がない場合はelseブロックが実行されます。 <ul>{{range .Items}}<li>{{.}}</li>{{else}}<li>アイテムなし</li>{{end}}</ul>
{{/* コメント */}} テンプレート内のコメント。出力には含まれません。 {{/* ここはコメントです */}}
{{define "name"}} ... {{end}} 名前付きのテンプレート(サブテンプレート)を定義します。 {{define "header"}}<header>ヘッダー</header>{{end}}
{{template "name" pipeline}} 名前付きテンプレートを呼び出して埋め込みます。pipelineでデータを渡すことができます。 {{template "header"}} または {{template "userinfo" .User}}

range アクションの例

package main

import (
	"html/template"
	"os"
)

func main() {
	templateString := `<h2>フルーツリスト</h2>
<ul>
{{range .Fruits}}
    <li>{{.}}</li>
{{else}}
    <li>フルーツはありません 🍎</li>
{{end}}
</ul>`

	tmpl, _ := template.New("range").Parse(templateString)

	data := struct {
		Fruits []string
	}{
		Fruits: []string{"リンゴ", "バナナ", "オレンジ"},
		// Fruits: []string{}, // こちらを有効にするとelseブロックが実行される
	}

	tmpl.Execute(os.Stdout, data)
}

テンプレートファイル 📄

通常、HTMLテンプレートはGoのコード内ではなく、別のファイル(.html.tmpl, .gohtml などの拡張子が使われます)に記述します。これにより、コードとビュー(見た目)を分離できます。

ファイルからテンプレートを読み込むには template.ParseFiles()template.ParseGlob() を使います。

例:

templates/index.html:

<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}}</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
  <section class="section">
    <div class="container">
      <h1 class="title">{{.Title}}</h1>
      <p class="subtitle">{{.Message}}</p>
      <div class="content">
        <h2>アイテムリスト</h2>
        <ul>
          {{range .Items}}
            <li>{{.}}</li>
          {{else}}
            <li>アイテムはありません。</li>
          {{end}}
        </ul>
      </div>
    </div>
  </section>
</body>
</html>

main.go:

package main

import (
	"html/template"
	"net/http"
	"log"
)

// テンプレートをキャッシュする(パフォーマンス向上のため)
// 開発中は毎回パースするようにしても良い
var templates = template.Must(template.ParseFiles("templates/index.html"))

type PageData struct {
	Title   string
	Message string
	Items   []string
}

func handler(w http.ResponseWriter, r *http.Request) {
	data := PageData{
		Title:   "ファイルからのテンプレート",
		Message: "ParseFilesを使って読み込みました!📁",
		Items:   []string{"Go", "HTML", "CSS"},
	}

	// "index.html"という名前で定義されたテンプレートを実行
	err := templates.ExecuteTemplate(w, "index.html", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		log.Printf("テンプレートの実行エラー: %v", err)
	}
}

func main() {
	http.HandleFunc("/", handler)
	log.Println("サーバー起動: http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

このコードを実行し、ブラウザで http://localhost:8080 にアクセスすると、index.html の内容がデータで埋められて表示されます。 template.Must() はパース時にエラーが発生した場合にpanicを起こすヘルパー関数で、初期化時にエラーを確実に検出したい場合に便利です。 ExecuteTemplate() は、複数のファイルがパースされた場合に、特定の名前のテンプレートを実行するために使用します。ParseFiles() でパースした場合、テンプレート名はファイル名になります。

コンテキストに応じたエスケープ (セキュリティ🛡️)

html/template の最も重要な機能の一つが、コンテキストに応じた自動エスケープです。これにより、開発者が特別な処理を意識しなくても、XSS攻撃のリスクを大幅に低減できます。

例えば、テンプレートに以下のようなデータを渡した場合:

data := struct {
    Comment string
}{
    Comment: "<script>alert('XSS攻撃!');</script>",
}

テンプレート内で {{.Comment}} と記述すると、出力されるHTMLは次のようになります。

&lt;script&gt;alert('XSS攻撃!');&lt;/script&gt;

<, >, ' などの特殊文字がHTMLエンティティに変換(エスケープ)されているため、ブラウザはこれをスクリプトとして実行せず、単なる文字列として表示します。

このエスケープは、HTMLの属性値、JavaScript内、CSS内など、埋め込まれる場所(コンテキスト)に応じて適切に行われます。

注意: 意図的にHTMLタグを出力したい場合(例えば、WYSIWYGエディタからの入力を表示する場合など)は、template.HTML 型を使用することでエスケープを回避できます。
例: data := struct{ SafeHTML template.HTML }{ SafeHTML: template.HTML("<strong>安全なHTML</strong>") }
ただし、ユーザー入力など信頼できないソースからのデータを template.HTML に渡すことは非常に危険であり、XSS脆弱性の原因となるため、絶対に避けてください。

テンプレート関数 ⚙️

テンプレート内で利用できるカスタム関数を定義することもできます。これにより、テンプレート内でのデータ整形や複雑なロジックの実装が可能になります。

関数は template.FuncMap 型(map[string]interface{})で定義し、テンプレートをパースする前に Funcs() メソッドで登録します。

例:日付をフォーマットする関数

package main

import (
	"html/template"
	"os"
	"time"
	"strings"
)

// カスタム関数: 日付を指定したフォーマットで返す
func formatDate(t time.Time, layout string) string {
	return t.Format(layout)
}

// カスタム関数: 文字列を大文字にする
func toUpper(s string) string {
	return strings.ToUpper(s)
}

func main() {
	// カスタム関数を登録するためのFuncMap
	funcMap := template.FuncMap{
		"fdate":  formatDate,
		"upper": toUpper,
	}

	// テンプレート文字列
	templateString := `現在時刻: {{.Now | fdate "2006年01月02日 15:04:05"}}
メッセージ: {{.Message | upper}}
`
	// Newでテンプレートを作成し、Funcsで関数を登録し、Parseで解析
	tmpl, err := template.New("customFuncs").Funcs(funcMap).Parse(templateString)
	if err != nil {
		panic(err)
	}

	// データ
	data := struct {
		Now     time.Time
		Message string
	}{
		Now:     time.Now(),
		Message: "custom functions demo!",
	}

	// 実行
	tmpl.Execute(os.Stdout, data)
}

テンプレート内では、{{ .Value | funcName arg1 arg2 }} のようにパイプライン(|)を使って関数を呼び出すことができます。関数の第一引数にはパイプラインの前の値が渡されます。

ネストされたテンプレート (テンプレートの部品化) 🧩

ウェブサイトでは、ヘッダーやフッターなど、複数のページで共通して使われる部分があります。html/template では、{{define "name"}} ... {{end}} で部品となるテンプレートを定義し、{{template "name" .}} でそれを呼び出すことができます。これにより、テンプレートの再利用性が高まり、管理がしやすくなります。

例:

templates/layout.html:

{{define "layout"}}
<!DOCTYPE html>
<html>
<head>
    <title>{{template "title" .}}</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
    {{template "header" .}}
    <section class="section">
        <div class="container">
            {{template "content" .}}
        </div>
    </section>
    {{template "footer" .}}
</body>
</html>
{{end}}

{{define "header"}}
<nav class="navbar is-info" role="navigation" aria-label="main navigation">
  <div class="navbar-brand">
    <a class="navbar-item" href="/">
      マイサイト 🚀
    </a>
  </div>
</nav>
{{end}}

{{define "footer"}}
<footer class="footer">
  <div class="content has-text-centered">
    <p>
      © 2025 My Website
    </p>
  </div>
</footer>
{{end}}

templates/index.html:

{{/* layout.html をベースとして利用 */}}
{{define "title"}}ホームページ{{end}}

{{define "content"}}
<h1 class="title">{{.PageTitle}}</h1>
<p>{{.Message}}</p>
{{end}}

main.go:

package main

import (
	"html/template"
	"log"
	"net/http"
)

var templates *template.Template

func init() {
	// ParseGlobで複数のテンプレートファイルを一度に読み込む
	// layout.html内の define が他のテンプレートから利用可能になる
	templates = template.Must(template.ParseGlob("templates/*.html"))
}

type IndexData struct {
	PageTitle string
	Message   string
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	data := IndexData{
		PageTitle: "ようこそ!",
		Message:   "これはネストされたテンプレートのサンプルです。",
	}
	// "layout" という名前のテンプレート(layout.html内で定義)を実行
	// layout内で呼び出される "title", "content" などは index.html で定義されたものが使われる
	err := templates.ExecuteTemplate(w, "layout", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		log.Printf("テンプレート実行エラー: %v", err)
	}
}

func main() {
	http.HandleFunc("/", indexHandler)
	log.Println("サーバー起動: http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

この例では、ParseGlob を使って templates ディレクトリ内の全ての .html ファイルを読み込んでいます。これにより、layout.html で定義された {{define "layout"}}, {{define "header"}}, {{define "footer"}} と、index.html で定義された {{define "title"}}, {{define "content"}} が一つのテンプレートセットとして扱われます。

indexHandler では ExecuteTemplate(w, "layout", data) を呼び出しています。これにより、まず layout テンプレートが実行されます。layout テンプレート内で {{template "title" .}}{{template "content" .}} が呼び出されると、同じテンプレートセット内にある titlecontent テンプレート(この場合は index.html で定義されたもの)が、渡されたデータ(.)を使って実行され、その結果が埋め込まれます。

templateアクションで呼び出す際に {{template "header"}} のようにデータを渡さない場合 (ドット.がない場合)、そのテンプレート内ではデータにアクセスできません。共通の静的な部品などで利用できます。

まとめ 🎉

html/template パッケージは、Goで動的なHTMLを安全かつ効率的に生成するための強力なツールです。

  • 基本的な値の埋め込み ({{.}}, {{.FieldName}})
  • 条件分岐 ({{if}}) や繰り返し ({{range}})
  • ファイルからのテンプレート読み込み (ParseFiles, ParseGlob)
  • 自動コンテキストエスケープによるXSS対策
  • カスタム関数の利用 (Funcs)
  • テンプレートの部品化と再利用 (define, template)

これらの機能を活用することで、メンテナンス性が高く安全なWebアプリケーションのフロントエンド部分を構築できます。ぜひ実際にコードを書いて試してみてください!🚀

コメント

タイトルとURLをコピーしました