markdown

はじめに (対象読者・この記事でわかること)

この記事は、Go 言語で Web アプリケーションや静的サイトを作成しているエンジニアを対象としています。特に html/template パッケージの range 構文 を使う際に、変数展開が期待通りに動かないという壁にぶつかった方に向けて執筆しました。
本稿を読むことで、以下のことができるようになります。

  • range で繰り返している要素のフィールドに安全にアクセスする方法
  • 変数スコープが狭くなることによる典型的なエラーの原因を把握
  • 実務で頻繁に使う「$」変数やパイプラインを駆使したテンプレート記法をマスター

執筆のきっかけは、社内プロジェクトでテンプレートに商品リストを表示しようとした際、{{ .Name }} が空文字になるという不具合に遭遇したことです。調べても情報が散在しており、同様の問題で悩む方が少なからずいると推測したため、体系的にまとめました。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。

  • Go 言語の基本的な文法とパッケージ利用方法
  • HTML/CSS の基礎(テンプレートで出力される HTML の構造をイメージできる程度)
  • go rungo build といった簡単なビルド・実行コマンドの操作

概要・背景

html/template は XSS 対策が自動で組み込まれた安全志向のテンプレートエンジンです。
構文としては {{ range .Items }} のようにスライスやマップを走査でき、各要素は暗黙的に .(ドット)で参照されます。

しかし、この暗黙的なスコープは 「現在のイテレーションの要素」 にだけ有効で、外側のコンテキストにある変数は直接参照できません。たとえば次のようなテンプレートを書いたとします。

Gotemplate
{{ range .Products }} <li>{{ .Name }} - {{ .Price }}</li> <p>カテゴリ: {{ .Category }}</p> <p>全体の合計: {{ .Total }}</p> {{ end }}

上記の .TotalProducts スライスの各要素に Total フィールドが無いため空文字になるか、テンプレート実行時にパニックします。
このように範囲内で「外側の変数」を利用したいケースは結構ありますが、range がスコープを閉じることが原因で期待通りに動作しません。

そこで Go では 「$」変数 を使って外側のコンテキストや一時的な値を保持します。$ は「現在のテンプレート実行時点での '.'」を固定する特殊変数であり、スコープが変わっても参照し続けることができます。

具体的な手順や実装方法

以下では、実際に「外側の変数 Totalrange 内で表示する」シナリオを例に、問題点の再現とその解決策をステップごとに解説します。

ステップ1 : サンプルデータとテンプレートの用意

まずは最小構成の Go プログラムとテンプレートファイルを作ります。

main.go

Go
package main import ( "html/template" "log" "os" ) type Product struct { Name string Price int Category string } type PageData struct { Products []Product Total int // 合計金額を外側で計算して保持 } func main() { data := PageData{ Products: []Product{ {Name: "Apple", Price: 120, Category: "Fruit"}, {Name: "Banana", Price: 80, Category: "Fruit"}, {Name: "Carrot", Price: 50, Category: "Vegetable"}, }, } // 合計金額を計算 for _, p := range data.Products { data.Total += p.Price } tmpl, err := template.ParseFiles("template.html") if err != nil { log.Fatalf("template parse error: %v", err) } if err := tmpl.Execute(os.Stdout, data); err != nil { log.Fatalf("template execute error: %v", err) } }

template.html(問題のあるバージョン)

Gotemplate
<ul> {{ range .Products }} <li>{{ .Name }} - {{ .Price }}円</li> <li>カテゴリ: {{ .Category }}</li> <li>合計金額: {{ .Total }}円</li> <!-- ← ここが空になる --> {{ end }} </ul>

このまま実行すると、{{ .Total }} が空文字になるか、template: template.html:6:18: map has no entry for key "Total" のようなエラーが発生します。

ステップ2 : $ 変数で外側コンテキストを固定

range の直前に外側の .(ここでは PageData 全体)を $ に代入すれば、内部でも参照できるようになります。

template.html(修正版)

Gotemplate
{{ $parent := . }} <!-- 現在のコンテキスト(PageData)を $parent に保存 --> <ul> {{ range .Products }} <li>{{ .Name }} - {{ .Price }}円</li> <li>カテゴリ: {{ .Category }}</li> <li>合計金額: {{ $parent.Total }}円</li> <!-- $parent を使って外側の Total にアクセス --> {{ end }} </ul>

ポイントは以下の通りです。

項目 説明
$parent := . 現在のコンテキスト(PageData)を $parent という名前の変数にバインド
$parent.Total range の内部からでも $parent が保持しているフィールドに直接アクセスできる
. の再定義は不要 range が内部で . を要素に上書きしても、$parent は元の . を保持し続ける

この修正で期待通りに合計金額が各行に表示されます。

ハマった点やエラー解決

1. $ 変数のスコープ誤解

$ は「現在のスコープで定義した直後から有効」になるため、以下のように range の内部で定義すると期待とは逆の挙動になります。

Gotemplate
{{ range .Products }} {{ $ := . }} <!-- NG: ここで $ が再定義され、外側のコンテキストが失われる --> {{ $parent.Total }} <!-- $parent が未定義になる --> {{ end }}

解決策$ 変数は range の外側、すなわち必要になる直前に定義する。定義位置を間違えると、スコープが上書きされて元の変数が失われます。

2. パイプラインと $ の併用ミス

{{ .Price | printf "$%d" }} のようにパイプラインを使うと . が別のオブジェクトに変わります。これを range 内で使う際に $ が必要になるケースがあります。

Gotemplate
{{ range .Products }} {{ $p := . }} <li>{{ $p.Name }} - {{ .Price | printf "$%d" }}</li> {{ end }}

上記は問題ありませんが、パイプラインの結果を再度 . で使おうとすると混乱します。

解決策必要なら $ に保存してから パイプラインの結果を利用する。例:

Gotemplate
{{ range .Products }} {{ $p := . }} {{ $price := .Price | printf "$%d" }} <li>{{ $p.Name }} - {{ $price }}</li> {{ end }}

3. マップのキーが文字列以外の場合

html/template ではマップのキーは必ず文字列でなければなりません。range でマップを走査しつつキーを取得したい場合は、$key, $value := . の形式で取得しますが、キーが整数や構造体の場合はテンプレート側で直接扱えません。

Gotemplate
{{ range $k, $v := .Map }} {{ $k }} : {{ $v }} {{ end }}

解決策:Go 側でキーを文字列に変換したマップを渡すか、struct にフィールド化してからテンプレートに渡す。

解決策まとめ

  1. 外側コンテキストを $ に保存
    gotemplate {{ $parent := . }}
  2. range の内部からは $parent.<field> で参照
    gotemplate {{ $parent.Total }}
  3. スコープの上書きを避ける
    $ の定義は range の外側(もしくは直前)で行う。
  4. パイプラインと併用するときは変数に格納
    gotemplate {{ $price := .Price | printf "$%d" }}
  5. マップキーは文字列にキャスト
    必要なら Go 側で変換してからテンプレートへ。

まとめ

本記事では、Go の html/template における range 内で外側変数が展開できない問題 を、実例とともに原因・対策を体系的に解説しました。

  • $ 変数で外側コンテキストを固定{{ $parent := . }} を使うだけで簡潔に解決
  • スコープの上書きに注意$ は定義位置が重要
  • パイプラインと変数代入の併用 → 中間結果は $ に格納してから利用

この記事を通して、テンプレートのスコープ管理がスムーズにできるようになり、デバッグ時間を大幅に削減 できるはずです。次回は「テンプレートでカスタム関数を登録して複雑なロジックを実装する」テーマで執筆予定です。

参考資料