イントロダクション 🎯
Go言語でWebアプリケーションを開発する際、動的にHTMLコンテンツを生成する必要がよくあります。例えば、ユーザー情報やデータベースから取得したデータをHTMLに埋め込んで表示したい場合などです。
このような場合に活躍するのが、Goの標準パッケージ html/template
です。このパッケージを使うと、テンプレートファイル(HTMLファイル)にGoのデータを埋め込み、安全かつ効率的にHTMLを生成できます。
特に重要なのは、html/template
がコンテキストに応じた自動エスケープ機能を持っている点です。これにより、クロスサイトスクリプティング(XSS)などの脆弱性を防ぎ、安全なWebアプリケーションを構築できます。💪
text/template
という似たパッケージもありますが、HTMLを出力する場合は、セキュリティ機能が組み込まれている html/template
を使うことが強く推奨されます。
基本的な使い方 🛠️
html/template
の基本的な流れは以下のようになります。
- テンプレート文字列やテンプレートファイルを準備する。
template.New()
でテンプレートオブジェクトを作成し、Parse()
やParseFiles()
,ParseGlob()
でテンプレートを解析する。- テンプレートに埋め込むデータ(構造体やマップなど)を用意する。
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は次のようになります。
<script>alert('XSS攻撃!');</script>
<
, >
, '
などの特殊文字がHTMLエンティティに変換(エスケープ)されているため、ブラウザはこれをスクリプトとして実行せず、単なる文字列として表示します。
このエスケープは、HTMLの属性値、JavaScript内、CSS内など、埋め込まれる場所(コンテキスト)に応じて適切に行われます。
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" .}}
が呼び出されると、同じテンプレートセット内にある title
や content
テンプレート(この場合は index.html
で定義されたもの)が、渡されたデータ(.
)を使って実行され、その結果が埋め込まれます。
template
アクションで呼び出す際に {{template "header"}}
のようにデータを渡さない場合 (ドット.
がない場合)、そのテンプレート内ではデータにアクセスできません。共通の静的な部品などで利用できます。
まとめ 🎉
html/template
パッケージは、Goで動的なHTMLを安全かつ効率的に生成するための強力なツールです。
- 基本的な値の埋め込み (
{{.}}
,{{.FieldName}}
) - 条件分岐 (
{{if}}
) や繰り返し ({{range}}
) - ファイルからのテンプレート読み込み (
ParseFiles
,ParseGlob
) - 自動コンテキストエスケープによるXSS対策
- カスタム関数の利用 (
Funcs
) - テンプレートの部品化と再利用 (
define
,template
)
これらの機能を活用することで、メンテナンス性が高く安全なWebアプリケーションのフロントエンド部分を構築できます。ぜひ実際にコードを書いて試してみてください!🚀
コメント