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

イントロダクション

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アプリケーションのフロントエンド部分を構築できます。ぜひ実際にコードを書いて試してみてください!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です