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