Goのhtml/templateパッケージの使い方まとめ
この記事は株式会社カオナビ Advent Calendar 2025 シーズン1の14日目です。
Goでもあまり使う機会がなさそう(?)な html/template ですが、最近Goでtemplateを使用した実装をたくさん行う機会がありました。
めったに使わないパッケージのため躓くポイントも多かったため色々な使い方をメモとして残しておきます。
基本的な使い方
最もシンプルな例は以下のような実装になります。
テンプレートファイルを読み込み、データを渡して出力するという基本的な流れです。
ParseFiles でテンプレートファイルを解析し、Execute でデータを適用してHTTPレスポンスとして書き込みます。
( html/template ということで一応用途が多そうな net/http を使用した例を書きましたが、以降は面倒なので os.Stdout に書き込む例にします)
package main
import (
"html/template"
"net/http"
)
func main() {
http.HandleFunc("/sample", func(w http.ResponseWriter, r *http.Request) {
// テンプレートの解析
t, err := template.ParseFiles("sample.html")
if err != nil {
panic(err)
}
// 引数のデータパラメータを適用し、io.Writerに書き込みを行う
err = t.Execute(w, "ハロー世界!!")
if err != nil {
panic(err)
}
})
if err := http.ListenAndServe(":8888", nil); err != nil {
panic(err)
}
}
テンプレートファイルは以下のように記述することで、Executeに渡したデータがそのまま出力されます。
<html><body>{{ . }}</body></html>
text/templateとhtml/templateの違い
text/template と html/template は似た名前のパッケージですが、 html/template は出力時にエスケープが自動で行われる点が異なります。
package main
import (
ht "html/template"
"os"
tt "text/template"
)
func main() {
// text/templateを使用
t1, _ := tt.ParseFiles("sample1.html")
t1.Execute(os.Stdout, "<script>alert('あなたは乗っ取られました')</script>")
// html/templateを使用
t2, _ := ht.ParseFiles("sample1.html")
t2.Execute(os.Stdout, "<script>alert('あなたは乗っ取られました')</script>")
}
出力は以下のようになります
$ go run main.go
# text/templateを使用
<script>alert('あなたは乗っ取られました')</script>
# html/templateを使用
<script>alert('あなたは乗っ取られました')</script>
htmlを出力する用途の場合は html/template を使用すると安全に値を取り扱うことができます。
エスケープが必要ないケースの場合は text/template を使用するとよいです。
データの出力
構造体の値を出力
用途としては一番多くなると思いますが、データとして構造体を渡すことができます。
func main() {
type s struct {
Name string
Age int
}
d := s{Name: "taro", Age: 22}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil { panic(err) }
}
構造体の値を出力する場合は以下のように . とフィールド名を組み合わせて使用します。
<html><body>
<p>{{ .Name }}は{{ .Age }}歳です</p>
</body></html>
ネストした構造体の値を出力
Favoriteのように構造体が構造体を持っている場合です。
func main() {
type s struct {
Name string
Age int
Favorite struct{ Name string }
}
d := s{Name: "taro", Age: 22, Favorite: struct{ Name string }{Name: "ラーメン"}}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil { panic(err) }
}
以下のように . 区切りでフィールド名を連結すればOKです
<html><body>
<p>{{ .Name }}は{{ .Age }}歳です</p>
<p>好きなものは{{ .Favorite.Name }}です</p>
</body></html>
構造体出力時の注意
テンプレートで使用する構造体のフィールドはpublicである必要があります。
また存在しないフィールドを指定することもできません。
func main() {
type s struct {
Name string
Age int
gender string
}
d := s{Name: "taro", Age: 22, gender: "man"}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil { panic(err) }
}
privateなフィールドを参照しようとした場合、Executeはエラーを返却します。
executing "sample1.html" at <.gender>: gender is an unexported field of struct type main.s
<html><body>
<p>{{ .Name }}は{{ .Age }}歳です</p>
<p>性別は{{ .gender }}です</p>
</body></html>
存在しないフィールドを指定した場合はこのようなエラーを返します。
executing "sample1.html" at <.Unknown>: can't evaluate field Unknown in type main.s
<html><body>
<p>{{ .Name }}は{{ .Age }}歳です</p>
<p>性別は{{ .Unknown }}です</p>
</body></html>
mapの要素を出力
func main() {
d := map[string]any{"name": "taro", "age": 22}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil { panic(err) }
}
構造体の値を出力と同様に . とmapのキーを組み合わせて使用します。
もちろんmapはunexportedの考慮は不要なので小文字でもOKです。
<html><body>
<p>{{ .name }}は{{ .age }}歳です</p>
</body></html>
また構造体とは異なり、存在しないキーを指定した場合でもExecuteはエラーを返しません。
存在しないキー(unknown)は空文字列が出力されます。
<html><body>
<p>{{ .name }}は{{ .age }}歳です</p>
<p>{{ .unknown }}は存在しません</p>
</body></html>
メソッドの実行結果を出力
以下のようにメソッド呼び出しした結果を出力することも可能です
func main() {
type s struct {
Error error
}
d := s{Error: fmt.Errorf("エラーです")}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil {
panic(err)
}
}
関数・メソッドの呼び出し時はカッコは記述しないという点に注意が必要です。
<html><body>
<p>エラーメッセージは「{{ .Error.Error }}」です</p>
</body></html>
もしも以下のように引数が必要な場合、
func main() {
type s struct {
Name Name
}
d := &s{Name: "taro"}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil {
panic(err)
}
}
type Name string
func (n *Name) Text(s string) string {
return fmt.Sprintf("%sに%sを連結", *n, s)
}
スペース区切りで引数を渡すことができます。
<html><body>
<p>{{ .Name.Text "hoge" }}</p>
</body></html>
繰り返し処理
func main() {
type s struct {
Favorite []string
}
d := s{Favorite: []string{"ラーメン", "寿司"}}
t, _ := template.ParseFiles("sample1.html")
if err := t.Execute(os.Stdout, d); err != nil { panic(err) }
}
{{ range データ }} ~ {{ end }} の形式で記述します。
<html><body>
{{ range .Favorite }}
<p>{{ . }}</p>
{{ end }}
</body></html>
Sliceの要素を変数に格納する
{{ range $変数名 := データ }} の形式にすることでループの値を変数に設定することも可能です。
<html><body>
{{ range $item := .Favorite }}
<p>{{ $item }}</p>
{{ end }}
</body></html>
一度変数に格納することで子rangeから親rangeの値が参照できるようになります。
. だとどのスコープの値を参照しているか分かりにくくなるため、rangeを使用する際は基本的に変数に値を設定するほうが良さそうです。
<html><body>
{{ range $parent := .Parents }}
<p>{{ $parent.Name }}</p>
{{ range $child := $parent.Children }}
<p>{{ $parent.Name }}の子要素は{{ $child.Name }}</p>
{{ end }}
{{ end }}
</body></html>
Sliceのindexも変数に格納する
Sliceのindexが必要な場合は {{ range $index, $変数名 := データ }} の形式にすることでループの値を変数に入れることも可能です。
<html><body>
{{ range $index, $item := .Favorite }}
<p>{{ $index }}: {{ $item }}</p>
{{ end }}
</body></html>
Sliceの長さを取得
len関数を使うことでSliceの長さを取得可能です
Sliceは {{ len .Slice }} 個の要素を持っています
余分な空白を削除して出力する
<html><body>
<p>-----</p>
{{ range .Favorite }}
<p>{{ . }}</p>
{{ end }}
<p>-----</p>
</body></html>
先程のような繰り返し処理で値を出力した場合、結果は以下のようにループの要素間に余分な改行が出力されます。
<html><body>
<p>-----</p>
<p>ラーメン</p>
<p>寿司</p>
<p>-----</p>
</body></html>
これを回避するにはハイフンありの {{- と -}} を使用します。
-
{{-は直前のテキストから末尾の空白文字が削除されます。 -
-}}は直後のテキストから先頭の空白文字がすべて削除されます。
先程の繰り返し処理で使用する場合は以下のように -}} を使用することで余分な空白が除去された状態で出力されます。
<html><body>
<p>-----</p>
{{ range .Favorite -}}
<p>{{ . }}</p>
{{ end -}}
<p>-----</p>
</body></html>
↓
<html><body>
<p>-----</p>
<p>ラーメン</p>
<p>寿司</p>
<p>-----</p>
</body></html>
条件分岐
if文による分岐は以下のように記述します。
<html><body>
{{ if .Value1 }}
Value1がtrue
{{ else if .Value2 }}
Value2がtrue
{{ else }}
それ以外
{{ end }}
</body></html>
論理演算子を使用する
if文を使うからにはand/or/notが必要になります。
それぞれ以下のように、 演算子 {条件1}... のように記述することで複雑な条件分岐に対応できます。
<html><body>
{{ if and .Value1 .Value2 }}
Value1, Value2がtrue!!
{{ end }}
{{ if or .Value1 .Value2 }}
Value1 または Value2がtrue!!
{{ end }}
{{ if not .Value1 }}
Value1 がfalse!!
{{ end }}
</body></html>
これらは条件値がゼロ値の場合は false と、ゼロ値以外を true とみなします。
比較演算子を使用する
使用可能な比較演算子は以下の通りです
-
eq:Returns the boolean truth of arg1 == arg2 -
ne:Returns the boolean truth of arg1 != arg2 -
lt:Returns the boolean truth of arg1 < arg2 -
le:Returns the boolean truth of arg1 <= arg2 -
gt:Returns the boolean truth of arg1 > arg2 -
ge:Returns the boolean truth of arg1 >= arg2
論理演算子と同様に以下のような使い方をします。
これらを使うことでさらに複雑な条件を記述することができるようになります
<html><body>
{{ if eq .Value1 .Value2 }}
Value1 と Value2が等しい!!
{{ end }}
{{ if ne .Value1 .Value2 }}
Value1 と Value2が等しくない!!
{{ end }}
</body></html>
演算の優先順位の制御
各演算子と () を使用し優先順位の制御も可能です。
以下のようにand演算子の条件として、eqとneの併用などより複雑な記述が可能になります。
<html><body>
{{ if and (eq .Value1 .Value2) (ne .Value1 .Value3) }}
Value1 と Value2が等しい かつ Value1 と Value3 が等しくない
{{ end }}
</body></html>
カスタム関数を登録
text/template には標準でグローバル関数としてlen, html, printなどの関数が用意されています。
さらに独自のカスタム関数を追加することができます。
template.New で template.Template を初期化後、
Funcsメソッドに template.FuncMap (実態は map[string]any ) を渡すことでカスタム関数が追加できます。
※必ずParseFilesを実行する前に呼び出す必要があります。(順番が逆だとエラーになります)
func main() {
type s struct {
Name string
}
d := &s{Name: "taro"}
t, err := template.New("sample1.html").
Funcs(template.FuncMap{
"toString": func(val any) string {
i, ok := val.(int)
if !ok {
return ""
}
return strconv.Itoa(i)
},
"toInt": func(val any) int {
s, ok := val.(string)
if !ok {
return 0
}
i, _ := strconv.Atoi(s)
return i
},
}).
ParseFiles("sample1.html")
if err != nil {
panic(err)
}
if err := t.Execute(os.Stdout, d); err != nil {
panic(err)
}
}
追加したカスタム関数は以下のようにテンプレート側から使用する事が可能になります。
<html><body>
{{ if eq (toString 1) "1" }}
文字列変換後の1は "1" と等しい!!
{{ end }}
{{ if ne (toInt "2") 1 }}
数値変換後の"2"は1と等しくない!!
{{ end }}
</body></html>
Discussion