text-template
Go text/template 総合ガイド — テンプレート語の意味論と周辺エコシステム
本ガイドは、Go標準ライブラリ text/template の全体像を、テンプレート語(テンプレート内で書く言語)の意味論と、このパッケージを土台にした周辺ツール群での実際の使われ方に重点を置いてまとめたものである。
掲載したテンプレート、Goコード、$ から始まる出力とエラーは、すべて Go 1.26.7(darwin/arm64、Apple M4 Max) で実際に実行して確認している。
目次
- はじめに
- アーキテクチャ
- テンプレート語の基礎
- ドットとスコープ
- 制御構造
- パイプラインと関数
- テンプレートの合成
- Go側のAPI
- html/template との違い
- 周辺ツールでの使われ方
- 落とし穴一覧
- まとめ
- 参考情報
1. はじめに
1.1 text/template とは
任意のテキストを、データと組み合わせて生成するためのテンプレートエンジンである。 Goの標準ライブラリに含まれ、外部依存がない。
t := template.Must(template.New("t").Parse("Hello, {{ .Name }}!\n"))
t.Execute(os.Stdout, map[string]string{"Name": "Ada"})
// → Hello, Ada!
設計の特徴は3つある。
| 特徴 | 内容 |
|---|---|
| 出力形式に無関心 | プレーンテキスト。HTMLもYAMLもJSONもただの文字列として扱う |
| チューリング完全でない | 再帰的なテンプレート呼び出しはできるが、任意の計算はできない |
| 拡張は関数マップのみ | 言語を拡張する唯一の手段が Funcs() である |
3つ目が本ガイドで最も強調したい点である。 Helm、consul-template、nomad-pack、kubectl、Docker、Prometheus — これらのテンプレート機能はすべて text/template に関数を足しただけであり、コア言語は同一である。したがって text/template の意味論を理解すれば、それらすべてに転用できる。
1.2 なぜこの知識が効くのか
インフラ運用の現場で遭遇するテンプレートの大半が text/template である。
| ツール | テンプレート機能 | デリミタ |
|---|---|---|
| Helm | チャートのマニフェスト生成 | {{ }} |
| consul-template | Consul/Vault の値を設定ファイルへ反映 | {{ }} |
| Nomad | ジョブ仕様の template スタンザ | {{ }} |
| nomad-pack | ジョブ仕様そのものの生成 | [[ ]] |
| kubectl | -o go-template | {{ }} |
| Docker | --format | {{ }} |
| Prometheus / Alertmanager | アラート通知の整形 | {{ }} |
| Grafana Loki / Promtail | ラベルの加工 | {{ }} |
| Terraform(の一部プロバイダ) | — | — |
「Helmのテンプレートが読めない」という悩みの大半は、Helm固有の話ではなく text/template の話である。 {{- が何をするのか、. がいつ変わるのか、and が何を返すのか — これらはすべて標準ライブラリの仕様である。
1.3 検証環境
$ go version
go version go1.26.7 darwin/arm64
本ガイドの検証には、次の形のハーネスを使った。
package main
import (
"os"
"text/template"
)
func main() {
src, _ := os.ReadFile(os.Args[1])
t, err := template.New("t").Parse(string(src))
if err != nil {
println("PARSE ERROR:", err.Error())
return
}
if err := t.Execute(os.Stdout, data); err != nil {
println("\nEXEC ERROR:", err.Error())
}
}
パースエラーと実行エラーを区別して表示している点が重要である。 §2.3 で述べるとおり、この区別が text/template を理解する鍵になる。
2. アーキテクチャ
2.1 4段のパイプライン
テンプレート文字列
│
↓ ① 字句解析(lexer)
トークン列 {{ }} の内外を切り分け、アクションを字句に分解
│
↓ ② 構文解析(parser)
AST text/template/parse パッケージのノード木
│
↓ ③ 名前解決・関数存在確認
検証済みAST この段で「関数が存在しない」等を検出する
│
↓ ④ 実行(exec)— データを与えて走らせる
出力テキスト reflect でデータを辿りながら Writer へ書く
①〜③が Parse、④が Execute である。 この境界がエラーの出方を決める。
2.2 AST を実際に覗く
text/template/parse は公開パッケージなので、構文木を直接観察できる。
tr, err := parse.Parse("demo", `A{{ if .X }}{{ .Y | upper }}{{ end }}B`,
"{{", "}}", map[string]any{"upper": strings.ToUpper})
dump(tr["demo"].Root, 0)
*parse.ListNode `A{{if .X}}{{.Y | upper}}{{end}}B`
*parse.TextNode `A`
*parse.IfNode `{{if .X}}{{.Y | upper}}{{end}}`
*parse.PipeNode `.X`
*parse.CommandNode `.X`
*parse.ListNode `{{.Y | upper}}`
*parse.ActionNode `{{.Y | upper}}`
*parse.PipeNode `.Y | upper`
*parse.CommandNode `.Y`
*parse.CommandNode `upper`
*parse.TextNode `B`
この木からテンプレート語の構造がそのまま読み取れる。
| ノード | 対応する構文 |
|---|---|
ListNode | テキストとアクションの列 |
TextNode | {{ }} の外側の生テキスト |
ActionNode | {{ ... }} 1つ |
PipeNode | パイプライン(| で連結されたコマンド列) |
CommandNode | パイプラインの1段(関数呼び出しまたは値) |
IfNode / RangeNode / WithNode | 制御構造 |
TemplateNode | {{ template "x" }} |
PipeNode が複数の CommandNode を持つという構造が、パイプラインの実体である。.Y | upper は「.Y を評価し、その結果を upper の最後の引数として渡す」となる。§6.1 で改めて触れる。
2.3 パース時エラーと実行時エラーの境界
この区別を誤ると、デバッグの方向を間違える。
パース時に判明するもの:
--- min / max 組み込み
PARSE ERROR: template: t:1: function "min" not defined
| パース時に検出 | 例 |
|---|---|
| 構文エラー | {{ if }} の閉じ忘れ、括弧の不一致 |
| 未定義の関数 | {{ min 1 2 }}(min は組み込みではない) |
| テンプレート名の重複定義(同一Parse内) | §7.4 |
実行時に判明するもの:
--- 存在しない struct フィールド
EXEC ERROR: template: t:1:4: executing "t" at <.Nope>: can't evaluate field Nope in type main.User
--- nil ポインタの参照
EXEC ERROR: template: t:1:12: executing "t" at <.Missing.City>: nil pointer evaluating *main.Address.City
--- 未定義テンプレートの呼び出し
EXEC ERROR: template: t:1:12: executing "t" at <{{template "nosuch"}}>: template "nosuch" not defined
| 実行時に検出 | 例 |
|---|---|
| フィールド・メソッドが存在しない | .Nope |
| nil ポインタの参照 | .Missing.City |
| 型の不一致 | eq 1 "1" |
| 関数が error を返した | {{ fail }} |
| 未定義テンプレートの呼び出し | {{ template "nosuch" }} |
| 引数の個数違い | call upper(引数不足) |
**「関数名の綴り間違いはパース時に落ちるが、テンプレート名の綴り間違いは実行時まで落ちない」**という非対称がある。これはHelmチャートで {{ include "mychart.labelz" . }} のような誤りが、そのコードパスを通るまで発覚しない理由である。
エラーメッセージの構造も覚えておく価値がある。
template: t:1:12: executing "t" at <.Missing.City>: nil pointer evaluating *main.Address.City
│ │ │ │ │ │
│ │ │ │ │ └─ 具体的な原因
│ │ │ │ └─ 問題のあった式
│ │ │ └─ 実行中のテンプレート名
│ │ └─ 列
│ └─ 行
└─ テンプレート名
行と列、式、原因がすべて出る。 巨大なHelmテンプレートでも位置が特定できる。
2.4 パースは1回、実行は何度でも
const src = `{{ range $i, $v := .Items }}{{ $i }}:{{ $v.Name }} {{ end }}`
func BenchmarkParseEachTime(b *testing.B) {
for i := 0; i < b.N; i++ {
t := template.Must(template.New("t").Parse(src))
t.Execute(io.Discard, data)
}
}
func BenchmarkParseOnce(b *testing.B) {
for i := 0; i < b.N; i++ {
pre.Execute(io.Discard, data) // pre はパッケージ変数
}
}
$ go test -bench=. -benchmem -run=XXX
goos: darwin
goarch: arm64
cpu: Apple M4 Max
BenchmarkParseEachTime-16 392786 3066 ns/op 4667 B/op 75 allocs/op
BenchmarkParseOnce-16 1969609 611.4 ns/op 528 B/op 13 allocs/op
BenchmarkConcurrentExecute-16 5412448 231.8 ns/op 528 B/op 13 allocs/op
パースを毎回やると約5倍遅く、アロケーションは6倍近くになる。 テンプレートはパッケージ変数か sync.Once で1回だけパースし、Execute を繰り返すのが定石である。
var tmpl = template.Must(template.New("t").Parse(src)) // 起動時に1回
template.Must は「パースに失敗したら panic する」ヘルパで、起動時に落としてしまうための道具である。テンプレートの構文エラーは実行時ではなくデプロイ時に発見したい、という思想である。
2.5 並行安全性
上のベンチマークの3つ目が b.RunParallel である。
BenchmarkConcurrentExecute-16 5412448 231.8 ns/op
パース済みのテンプレートに対する Execute は並行安全である。 16並列で問題なく動き、スループットも上がっている。
ただし安全なのは Execute だけである。
| メソッド | 並行安全 |
|---|---|
Execute / ExecuteTemplate | ✓(パース完了後) |
Parse | ✗ |
Funcs | ✗ |
Option / Delims | ✗ |
Clone | ✗(元テンプレートを読む) |
「起動時に構築を完了させ、以後は Execute だけ」という使い方が前提になっている。 リクエストごとに Funcs を足すような設計は競合する。リクエスト固有の関数が必要なら Clone してから Funcs するが、Clone 自体のコストがかかる。
3. テンプレート語の基礎
3.1 アクションとテキスト
{{ }} の外側はそのまま出力され、内側は「アクション」として評価される。
A{{ .Name }}B
│ └─ アクション
└─ 生テキスト
アクションの中の空白は自由である。 {{.Name}} と {{ .Name }} は同一である。慣習としては空白を入れる(Helmの公式チャートもそうしている)。
3.2 コメント
--- コメント
入力: A{{/* これはコメント */}}B{{- /* トリム付きコメント */ -}}C
出力: ABC
{{/* ... */}} がコメントで、出力に現れない。 トリム記法(§5.4)と併用できる。
コメントはアクションの一種なので、テキストの途中に置くと「アクションがあった」痕跡が残らない(前後のテキストがそのまま連結される)。
3.3 データの参照
type Address struct{ City, Zip string }
type User struct {
Name string
Age int
Tags []string
Addr *Address
Meta map[string]string
}
func (u User) Greet() string { return "Hello, " + u.Name }
func (u User) Label(p string) string { return p + ":" + u.Name }
Name: {{ .Name }}
Age: {{ .Age }}
Nested: {{ .Addr.City }} / {{ .Addr.Zip }}
Map: {{ .Meta.team }} / {{ index .Meta "tier" }}
Slice: {{ index .Tags 0 }}
Method: {{ .Greet }}
Arg: {{ .Label "user" }}
Len: {{ len .Tags }}
Name: Ada
Age: 36
Nested: London / NW1
Map: core / 1
Slice: math
Method: Hello, Ada
Arg: user:Ada
Len: 2
読みどころが4つある。
① ポインタは自動的にデリファレンスされる。 .Addr は *Address だが .Addr.City と書ける。
--- ポインタの自動デリファレンス
{{ .Addr.City }} / {{ (.Addr).Zip }}
→ London / NW1
② マップのキーは .Key でも index でも引ける。 ただし挙動が違う(§3.4)。
③ メソッドは引数なしなら .Greet だけで呼ばれる。 括弧は不要というより、書けない。
④ 引数つきメソッドは .Label "user" と空白区切りで書く。 これは関数呼び出しの構文と同じである。
インデックスの多段も可能である。
{{ index (dict "k" (dict "j" "deep")) "k" "j" }}
→ deep
3.4 参照の失敗はどうなるか — 3つの非対称
ここが実務で最も混乱を招く部分である。 「存在しないものを参照したとき」の挙動が、対象によって3通りに分かれる。
① struct の存在しないフィールド → 実行時エラー
入力: [{{ .Nope }}]
出力: [
EXEC ERROR: template: t:1:4: executing "t" at <.Nope>: can't evaluate field Nope in type main.User
② map の存在しないキー → エラーにならない。ただし記法で結果が違う
入力: [{{ .Meta.nosuch }}] [{{ index .Meta "nosuch" }}]
出力: [<no value>] []
.Meta.nosuch は <no value> という文字列を出力し、index .Meta "nosuch" は空文字列を出力する。 同じ「キーがない」状態なのに出力が違う。
これは実害のある差である。 設定ファイルを生成しているときに <no value> という文字列が混入すると、下流のパーサが失敗する。あるいは失敗せずに <no value> という値で動いてしまう。
対策は3つある。
① index を使う → 空文字列になる
② Option("missingkey=zero") を設定 → 型のゼロ値になる(§8.3)
③ Option("missingkey=error") → 実行時エラーにする(§8.3)
③が最も安全である。 設定生成では「静かに空になる」より「落ちる」ほうがよい。
③ nil ポインタ → 参照するとエラー、そのまま出力すると <nil>
入力: [{{ .Missing.City }}]
出力: [
EXEC ERROR: template: t:1:12: executing "t" at <.Missing.City>: nil pointer evaluating *main.Address.City
入力: [{{ .Missing }}]
出力: [<nil>]
まとめると次のようになる。
| 参照 | 対象が無いとき | 危険度 |
|---|---|---|
.Field(struct) | 実行時エラー | 低(すぐ気づく) |
.Key(map) | <no value> を出力 | 高(静かに壊れる) |
index .Map "k"(map) | 空文字列 | 中 |
.Ptr.Field(nil ポインタ) | 実行時エラー | 低 |
.Ptr(nil ポインタ) | <nil> を出力 | 高 |
map と nil ポインタが静かに文字列を出力するのが要注意である。<no value> と <nil> を生成物にgrepするのは、実務的に有効な検査である。
3.5 メソッド呼び出しの規則
呼べるメソッドには条件がある。
--- 値レシーバに対するポインタメソッド
入力: {{ .PtrOnly }} // func (u *User) PtrOnly() string
出力: EXEC ERROR: can't evaluate field PtrOnly in type main.User
--- 未公開メソッド
入力: {{ .unexported }}
出力: EXEC ERROR: can't evaluate field unexported in type main.User
--- error を返すメソッド
入力: {{ .Fail }} // func (u User) Fail() (string, error)
出力: EXEC ERROR: template: t:1:3: executing "t" at <.Fail>: error calling Fail: method failed
| 条件 | 呼べるか |
|---|---|
| 公開メソッド(大文字始まり) | ✓ |
| 未公開メソッド | ✗ |
| 値を渡していて、ポインタレシーバのメソッド | ✗ |
| ポインタを渡していて、値レシーバのメソッド | ✓ |
| 戻り値が1つ | ✓ |
戻り値が (T, error) | ✓(error が非nilなら実行中断) |
| 戻り値が3つ以上 | ✗ |
「ポインタを渡すか値を渡すか」でメソッドの可用性が変わる点が実装者側の注意点である。テンプレートに渡すデータは、メソッドを持たせるならポインタで渡すほうが安全である。
(T, error) を返せるのは強力である。 関数やメソッドがエラーを返すと Execute 全体が失敗するため、「必須項目が空なら生成を止める」という制御ができる。Helmの required がこの仕組みである(§10.4)。
4. ドットとスコープ
4.1 . は「現在の値」
. はテンプレート実行中の「カーソル」であり、固定ではない。 range、with、template の3つが . を差し替える。
Execute(w, data) → . = data
{{ range .Items }} → . = Items の各要素
{{ with .Addr }} → . = Addr
{{ template "x" .Foo }} → 呼ばれた側の . = .Foo
Helmテンプレートで {{ include "x" . }} の末尾に . を書く理由がこれである。 引数を渡さないと、呼ばれた側の . が nil になる。
--- 引数なしの template 呼び出しは . が nil になる
入力: {{ define "d" }}[dot={{ . }}]{{ end }}{{ template "d" }}|{{ template "d" . }}
出力: [dot=<no value>]|[dot=map[Fn:0x1002ae080 M:map[...] Name:Ada]]
4.2 $ はルート
$ は Execute に渡された最上位の値を常に指す。
入力: {{ range .Tags }}{{ . }}@{{ $.Name }} {{ end }}
出力: math@Ada eng@Ada
range の中で . は要素になっているが、$.Name でルートの Name に届く。range の中からグローバルな設定値を参照する定型がこれである。 Helmでいえば {{ $.Values.global.registry }} のような書き方になる。
4.3 変数の宣言と代入
--- 変数の宣言と再代入(1.11+)
入力: {{ $x := 1 }}{{ $x }} {{ $x = 2 }}{{ $x }}
{{ $s := "" }}{{ range .Tags }}{{ $s = print $s . "," }}{{ end }}accum={{ $s }}
出力: 1 2
accum=math,eng,
| 記法 | 意味 | 導入 |
|---|---|---|
$x := 値 | 宣言(新しい変数) | 当初から |
$x = 値 | 代入(既存変数の更新) | Go 1.11 |
= による代入は Go 1.11 で追加された。 それ以前は「ループ内で値を積み上げる」ことができなかった。上の accum の例が典型的な用途である。
宣言アクションは何も出力しない。 {{ $x := 1 }} は空文字列を出力する。
4.4 スコープ規則
--- スコープ: range 内の := は外に漏れない
入力: {{ $n := 0 }}{{ range .Tags }}{{ $n := 99 }}{{ end }}n={{ $n }}
出力: n=0
変数のスコープは、宣言されたブロックの終わりまでである。 range / if / with の内側で := すると、そのブロック内だけの新しい変数になる。
ループの外に結果を持ち出したいなら = を使う。
{{ $n := 0 }}
{{ range .Items }}{{ $n = add $n 1 }}{{ end }} ← = なら外の $n を更新
{{ $n }}
この規則を知らないと「ループでカウントしたのに0のまま」という定番のバグを踏む。
5. 制御構造
5.1 if と真偽判定
{{ if 条件 }}...{{ else if 条件 }}...{{ else }}...{{ end }}
真偽判定は「空かどうか」で決まる。 Goの bool だけでなく、あらゆる型が判定対象になる。
入力: zero-int:[{{ if 0 }}T{{ else }}F{{ end }}] empty-str:[{{ if "" }}T{{ else }}F{{ end }}]
出力: zero-int:[F] empty-str:[F]
「空」の定義は次の通りである。
| 型 | 空とみなされる値 |
|---|---|
bool | false |
| 数値 | 0 |
| 文字列 | "" |
| スライス / マップ / 配列 | 長さ0 |
| ポインタ / インターフェース | nil |
| 構造体 | 常に非空(フィールドが全てゼロでも真) |
構造体が常に真になる点に注意が必要である。 {{ if .Config }} は Config が構造体なら常に通る。ポインタにしておけば nil 判定ができる。
5.2 range
range は最も機能が多い制御構造である。
--- index と value
入力: {{ range $i, $v := .Tags }}{{ $i }}={{ $v }} {{ end }}
出力: 0=math 1=eng
--- else(対象が空のとき)
入力: {{ range .Empty }}{{.}}{{ else }}(no items){{ end }}
出力: (no items)
break / continue は Go 1.18 で追加された。
--- break / continue(1.18で追加)
入力: {{ range $i, $v := .Tags }}{{ if eq $i 1 }}{{ break }}{{ end }}{{ $v }} {{ end }}|
{{ range $i, $v := .Tags }}{{ if eq $i 0 }}{{ continue }}{{ end }}{{ $v }} {{ end }}
出力: math |
eng
整数を直接 range できる。
入力: {{ range 3 }}x{{ end }}
出力: xxx
入力: {{ range $i := 3 }}{{ $i }},{{ end }}
出力: 0,1,2,
マップを range するとキーでソートされる。 これはGoの言語仕様(マップの反復順序は不定)とは逆の挙動で、テンプレートの出力を決定的にするための意図的な設計である。
--- range over map はキーでソートされる(3回実行)
入力: {{ range $k, $v := .M }}{{ $k }}={{ $v }} {{ end }} // M は zebra/apple/mango/banana
出力: apple=2 banana=4 mango=3 zebra=1
apple=2 banana=4 mango=3 zebra=1
apple=2 banana=4 mango=3 zebra=1
printf "%v" でもマップはソートされる(fmt 側の挙動、Go 1.12以降)。
入力: {{ printf "%v" .M }}
出力: map[apple:2 banana:4 mango:3 zebra:1]
この性質のおかげで、生成した設定ファイルをGitにコミットして差分を見る運用が成立する。 ソートされなければ、実行するたびに順序が変わって差分だらけになる。
range できる型をまとめる。
| 型 | 1変数 | 2変数 |
|---|---|---|
| スライス / 配列 | 要素 | インデックス, 要素 |
| マップ | 値 | キー(ソート済), 値 |
| チャネル | 受信値 | (不可) |
| 整数 | 0..n-1 | インデックス(1変数と同じ) |
5.3 with
with は「値が空でなければ . を差し替えて実行する」構文である。
入力: {{ with .Addr }}{{ .City }}{{ else }}no addr{{ end }}
{{ with .Missing }}{{ .City }}{{ else }}no addr{{ end }}
出力: London
no addr
else 節は Go 1.11 で追加された。 それ以前は with に else が書けなかった。
変数束縛もできる。
入力: {{ with $a := .Addr }}{{ $a.City }}/{{ .Zip }}{{ end }}
出力: London/NW1
$a と . の両方が使える。深いパスを繰り返し書くのを避けるのが主な用途である。
{{ with .Values.image }}
image: {{ .repository }}:{{ .tag }}
{{ end }}
with は nil ガードも兼ねる。 上の例で .Values.image が nil なら中身はスキップされ、.repository の nil 参照エラーが起きない。consul-template の {{ with secret "..." }} はこの性質を使っている(§10.5)。
5.4 空白制御
生成物の見た目を制御する、実務で最も使う機能である。
--- 空白制御なし(· は空白、¶ は行末)
入力: A
{{ if .Admin }}
X
{{ end }}
B
出力: A¶
¶
X¶
¶
B¶
アクションが書かれていた行に、空行が残る。 {{ if }} 自体は何も出力しないが、その前後の改行はテキストとして残るためである。
{{- は直前の空白(改行を含む)を、-}} は直後の空白を削除する。
--- 左トリムのみ
入力: A
{{- if .Admin }}
X
{{- end }}
B
出力: A¶
X¶
B¶
--- 両側トリム
入力: A
{{- if .Admin -}}
X
{{- end -}}
B
出力: AXB¶
行内の空白も削除される。
入力: A {{- "X" -}} B
出力: AXB
| 記法 | 効果 |
|---|---|
{{- | アクションの直前の連続する空白・タブ・改行を削除 |
-}} | アクションの直後の連続する空白・タブ・改行を削除 |
注意点は2つある。
① - とアクションの間に空白が必要である。 {{-if}} は不正で、{{- if}} と書く。逆に {{ - 1 }} は「マイナス1」であってトリムではない。
② トリムは「連続する空白すべて」を消す。 1つだけ消すのではない。A {{- "X" }} は空白3つすべてが消える。
YAMLを生成する場合、この制御が生死を分ける。
# 悪い例 — 空行が入って YAML が壊れる可能性
metadata:
labels:
{{ range $k, $v := .Labels }}
{{ $k }}: {{ $v }}
{{ end }}
# 良い例
metadata:
labels:
{{- range $k, $v := .Labels }}
{{ $k }}: {{ $v }}
{{- end }}
Helmチャートが {{- を多用しているのは、YAMLのインデントと空行が意味を持つためである。
6. パイプラインと関数
6.1 パイプライン
| は「左の結果を、右の関数の最後の引数として渡す」演算子である。
--- パイプライン
入力: {{ "hello" | upper }} / {{ upper "hello" }} / {{ repeat "ab" 3 | upper }}
出力: HELLO / HELLO / ABABAB
{{ "hello" | upper }} ≡ {{ upper "hello" }}
{{ .X | f "a" }} ≡ {{ f "a" .X }} ← 最後の引数になる
{{ .X | f "a" | g "b" }} ≡ {{ g "b" (f "a" .X) }}
「最後の引数」であることが重要である。 sprig の関数群がこの規約に合わせて引数順を設計しているため、{{ .Value | default "fallback" }} のような自然な書き方ができる(§10.3)。
括弧で優先順位を明示できる。
入力: {{ (repeat "-" 5) }} / {{ printf "%s" (upper .name) }}
出力: ----- / ADA
引数の中に関数呼び出しを書くには括弧が必須である。 {{ printf "%s" upper .name }} は「printf に3つの引数を渡す」と解釈されてしまう。
6.2 組み込み関数は19個しかない
text/template の組み込み関数は、ソース上で以下の19個のみである。
// go1.26.7/src/text/template/funcs.go
return FuncMap{
"and": and,
"call": emptyCall,
"html": HTMLEscaper,
"index": index,
"slice": slice,
"js": JSEscaper,
"len": length,
"not": not,
"or": or,
"print": fmt.Sprint,
"printf": fmt.Sprintf,
"println": fmt.Sprintln,
"urlquery": URLQueryEscaper,
// Comparisons
"eq": eq, // ==
"ge": ge, // >=
"gt": gt, // >
"le": le, // <=
"lt": lt, // <
"ne": ne, // !=
}
| 分類 | 関数 |
|---|---|
| 論理 | and, or, not |
| 比較 | eq, ne, lt, le, gt, ge |
| 出力整形 | print, printf, println |
| データ操作 | index, slice, len |
| 呼び出し | call |
| エスケープ | html, js, urlquery |
文字列操作関数が1つも無い。 upper、trim、replace、split、join — すべて存在しない。算術関数も無い。add、sub、mul も無い。
この極端な最小主義が、Funcs() による拡張を必須にしている。 そして「みんなが同じ関数を実装する」ことになり、その共通実装として sprig が事実上の標準になった(§10.3)。
min / max も組み込みではない。
入力: min={{ min 3 1 2 }} max={{ max 3 1 2 }}
出力: PARSE ERROR: template: t:1: function "min" not defined
Go言語には 1.21 で min/max 組み込みが入ったが、テンプレートには入っていない。 混同しやすい点である。
6.3 and / or は真偽値を返さない
これは最も誤解されている挙動である。
入力: [{{ and "a" "b" }}] [{{ or "" "fallback" }}] [{{ not "" }}]
出力: [b] [fallback] [true]
| 関数 | 戻り値 |
|---|---|
and a b c | 最初の空の引数、すべて非空なら最後の引数 |
or a b c | 最初の非空の引数、すべて空なら最後の引数 |
not a | 真偽値(bool) |
and / or は Go の && / \|\| ではなく、Python の and / or に近い。 値を返す。
この性質は実用的である。
{{ or .Values.override .Values.default "hardcoded" }}
→ 最初に見つかった非空の値を採用する
しかし出力に使うと事故になる。
{{ if and .A .B }}...{{ end }} ← 条件として使うなら問題ない
{{ and .A .B }} ← 出力すると .B の値が出る(true ではない)
真偽値が欲しいなら not (not x) を使うというイディオムがある。
6.4 短絡評価
Go 1.18 で and / or は短絡評価になった。
入力: and short-circuit: {{ and false .Missing.City }}
or short-circuit: {{ or true .Missing.City }}
出力: and short-circuit: false
or short-circuit: true
.Missing.City は nil ポインタ参照なので、評価されればエラーになる。 それが起きていないので、短絡していることが確認できる。
Go 1.18 より前は両辺が必ず評価された。 つまり次のような nil ガードが動かなかった。
{{ if and .Addr .Addr.City }}...{{ end }}
古いGoで書かれたテンプレートには、この制約を回避するための入れ子の if や with が残っていることがある。
{{ with .Addr }}{{ if .City }}...{{ end }}{{ end }} ← 旧来の書き方
{{ if and .Addr .Addr.City }}...{{ end }} ← 1.18 以降はこれで動く
6.5 print / printf / println の細部
--- printf の verb
入力: {{ printf "%s|%q|%d|%05.2f|%v|%T" "a" "b" 42 3.14159 .n .n }}
出力: a|"b"|42|03.14|3|int
printf は fmt.Sprintf そのものである。 %v、%q、%T、幅・精度指定がすべて使える。
--- print / println
入力: [{{ print "a" "b" 1 2 }}] [{{ print "a" 1 "b" }}] [{{ println "x" }}]
出力: [ab1 2] [a1b] [x
]
print は fmt.Sprint であり、「両隣が文字列でないときだけ空白を入れる」という規則を持つ。
print "a" "b" 1 2 → "ab1 2" ← 1 と 2 の間だけ空白
print "a" 1 "b" → "a1b" ← 空白なし
この規則を知らないと、文字列連結で意図しない空白が入る。 確実に連結したいなら printf "%s%s" を使う。
println は末尾に改行を付ける。 テンプレート内で改行を出すのに使えるが、{{ "\n" }} のほうが意図が明確である。
6.6 比較関数と型
入力: eq={{ eq 1 1 }} ne={{ ne 1 2 }} lt={{ lt 1 2 }} ge={{ ge 2 2 }} eq-multi={{ eq 3 1 2 3 }}
出力: eq=true ne=true lt=true ge=true eq-multi=true
eq は複数引数を取れる。 eq x a b c は「x が a、b、c のいずれかに等しい」を意味する。in 演算子の代用になる。
{{ if eq .Env "dev" "stg" "test" }}非本番{{ end }}
型が違うと比較できない。
入力: {{ eq 1 "1" }}
出力: EXEC ERROR: template: t:1:3: executing "t" at <eq 1 "1">: error calling eq: incompatible types for comparison: int and string
JSONやYAML由来のデータでこれを踏みやすい。 1 として読まれた値と "1" を比較しようとして落ちる。printf "%v" で文字列化してから比較するのが回避策になる。
{{ if eq (printf "%v" .Version) "1" }}...{{ end }}
6.7 call と関数値
call は「関数値」を呼ぶための関数である。
--- call には関数値が必要
入力: {{ call .Fn "x" }} // .Fn は strings.ToUpper(データとして渡した関数値)
出力: X
FuncMap に登録した関数名は call に渡せない。
入力: {{ call upper "x" }}
出力: EXEC ERROR: template: t:1:33: executing "t" at <upper>: wrong number of args for upper: want 1 got 0
理由は、テンプレート内で upper と書いた時点で「引数0個の呼び出し」として評価されるためである。call の用途は「データ構造の中に関数が入っている場合」に限られる。
6.8 index と slice
入力: {{ index (dict "k" (dict "j" "deep")) "k" "j" }}
出力: deep
入力: {{ slice "abcdef" 1 3 }}
出力: bc
入力: {{ slice (dict) }}
出力: EXEC ERROR: error calling slice: can't slice item of type map[string]interface {}
| 関数 | 対象 | 意味 |
|---|---|---|
index x k1 k2 ... | マップ / スライス / 配列 / 文字列 | 多段のインデックス参照 |
slice x lo hi | スライス / 配列 / 文字列 | 部分列(x[lo:hi]) |
len x | 文字列 / スライス / マップ / 配列 / チャネル | 長さ |
index は「マップのキーが識別子として不正な場合」に必須である。 .Meta.my-key は書けない(- が演算子と解釈される)ので index .Meta "my-key" とする。Kubernetesのアノテーション(prometheus.io/scrape など)を参照するときに必ず使う。
{{ index .metadata.annotations "prometheus.io/scrape" }}
6.9 エスケープ関数
入力: {{ html "<b>&</b>" }} | {{ js "a'b" }} | {{ urlquery "a b&c" }}
出力: <b>&</b> | a\'b | a+b%26c
これらは手動で呼ぶものである。 text/template は自動エスケープしない(§9)。
7. テンプレートの合成
7.1 define / template / block
{{- define "row" -}}
- {{ .Name }} x{{ .Qty }}
{{ end -}}
{{- block "header" . -}}
== {{ .Title }} ==
{{ end -}}
{{ range .Items }}{{ template "row" . }}{{ end -}}
{{ range .Empty }}{{ template "row" . }}{{ else }}(no items)
{{ end -}}
[defined templates]
"row"
"header"
"t"
[output]
== Report ==
- alpha x2
- beta x0
(no items)
| 構文 | 意味 |
|---|---|
{{ define "name" }}...{{ end }} | 名前付きテンプレートを定義する(その場では実行しない) |
{{ template "name" arg }} | 定義済みテンプレートを呼び出す |
{{ block "name" arg }}...{{ end }} | 定義と呼び出しを同時に行う(既定の実装つき) |
block は define + template の糖衣である。
{{ block "x" . }}default{{ end }}
≡
{{ define "x" }}default{{ end }}{{ template "x" . }}
t.Templates() で定義済みのテンプレート一覧が取れる。 上の例では row、header、そして本体の t が返っている。block も名前付きテンプレートとして登録される点が確認できる。
7.2 継承パターン
block で既定を用意し、define で上書きするのが Go 流のテンプレート継承である。
const base = `{{ block "title" . }}DEFAULT TITLE{{ end }}
body: {{ .Body }}
{{ block "footer" . }}-- default footer --{{ end }}`
const child = `{{ define "title" }}CHILD TITLE{{ end }}`
=== base only
DEFAULT TITLE
body: hello
-- default footer --
=== base + child override
CHILD TITLE
body: hello
-- default footer --
title だけが差し替わり、footer は既定のまま残る。 これがレイアウト継承の実装である。
7.3 Clone の役割
Parse は受け側のテンプレートを破壊的に変更する。 したがって1つのベースから複数の子を作るには Clone が必要になる。
--- Clone で2つの子を作る
[child1][child2] base=[base]
--- Clone せず2つの子を作ろうとすると
base after two overrides=[child2]
base := template.Must(template.New("b").Parse(`[{{ block "slot" . }}base{{ end }}]`))
// Clone すれば base は無傷、子は独立
c1 := template.Must(template.Must(base.Clone()).Parse(`{{ define "slot" }}child1{{ end }}`))
c2 := template.Must(template.Must(base.Clone()).Parse(`{{ define "slot" }}child2{{ end }}`))
// → [child1] [child2]、base は [base] のまま
// Clone しないと base 自身が上書きされ、2つ目が勝つ
template.Must(base.Parse(`{{ define "slot" }}child1{{ end }}`))
template.Must(base.Parse(`{{ define "slot" }}child2{{ end }}`))
// → base は [child2]
Webアプリでレイアウト+ページの組み合わせを作るとき、Clone を忘れると全ページが最後にパースしたページになる。 典型的なバグである。
7.4 再定義の規則 — 3つの非対称
同名テンプレートの再定義は、状況によって「エラー」「後勝ち」「無視」に分かれる。
① 同一の Parse 呼び出し内で2回 → パースエラー
入力: {{ define "d" }}one{{ end }}{{ define "d" }}two{{ end }}
出力: ERROR: template: x:1: template: multiple definition of template "d"
② Parse を分けて2回 → 後勝ち
t.Parse(`{{ define "d" }}one{{ end }}{{ template "d" }}`)
t.Parse(`{{ define "d" }}two{{ end }}`)
→ two
③ 空の define での上書き → 無視される
base := `[{{ block "slot" . }}base{{ end }}]`
base.Parse(`{{ define "slot" }}{{ end }}`)
→ [base] ← 上書きされない
③は意図的な仕様である。 公式ドキュメントに次の記述がある。
a template definition with a body containing only white space and comments is considered empty and will not replace an existing template's body (本体が空白とコメントのみのテンプレート定義は空とみなされ、既存のテンプレート本体を置き換えない)
「ブロックを空にして消す」ことはできない。 空にしたいなら空白以外の何か(例えばコメントではなく {{ "" }})を置く必要がある。
この3つの非対称が、Helmのようなテンプレート群を扱うときの混乱源になる。 「上書きしたはずなのに効かない」の原因が③であることは少なくない。
7.5 ParseFiles / ParseGlob / ParseFS と命名の罠
=== ParseFiles: テンプレート名はベースファイル名になる
t.Name() = base.tmpl
"base.tmpl"
"child.tmpl"
"page.tmpl"
"body"
ファイルから読むと、テンプレート名は「ディレクトリを除いたベースファイル名」になる。 tpl/base.tmpl は base.tmpl という名前になる。
そして最大の罠がこれである。
=== template.New(name).ParseFiles の罠
t.Name() = myname
Execute → [EXEC ERROR: template: myname: "myname" is an incomplete or empty template]
// 動かない
t := template.Must(template.New("myname").ParseFiles("tpl/page.tmpl"))
t.Execute(os.Stdout, data) // → "myname" is an incomplete or empty template
// 動く(名前を指定しない)
t := template.Must(template.ParseFiles("tpl/page.tmpl"))
t.Execute(os.Stdout, data)
// 動く(名前を明示して実行する)
t := template.Must(template.New("myname").ParseFiles("tpl/page.tmpl"))
t.ExecuteTemplate(os.Stdout, "page.tmpl", data)
template.New("myname") で作った時点で「myname という空のテンプレート」が本体になり、Execute はそれを実行しようとする。 ファイルから読んだ内容は page.tmpl という別名で登録されているため、本体は空のままである。
ParseFS は embed と組み合わせて使う。
//go:embed tpl/*.tmpl
var fsys embed.FS
e := template.Must(template.ParseFS(fsys, "tpl/*.tmpl"))
e.ExecuteTemplate(os.Stdout, "page.tmpl", data)
バイナリにテンプレートを埋め込めるので、単一バイナリで配布するツールでは標準的な手法である。
Templates() の順序は保証されない。
=== Templates() の順序は安定か(3回)
body,base.tmpl,child.tmpl,page.tmpl
page.tmpl,body,base.tmpl,child.tmpl
base.tmpl,body,child.tmpl,page.tmpl
実行ごとに順序が変わる(内部がマップのため)。テンプレート一覧を表示する用途では自分でソートする必要がある。
8. Go側のAPI
8.1 主要な型とメソッド
type Template struct { /* ... */ }
// 生成
func New(name string) *Template
func Must(t *Template, err error) *Template
// パース
func (t *Template) Parse(text string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)
func ParseGlob(pattern string) (*Template, error)
func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
// 設定
func (t *Template) Funcs(funcMap FuncMap) *Template
func (t *Template) Option(opt ...string) *Template
func (t *Template) Delims(left, right string) *Template
// 複製・検査
func (t *Template) Clone() (*Template, error)
func (t *Template) Lookup(name string) *Template
func (t *Template) Templates() []*Template
func (t *Template) Name() string
func (t *Template) DefinedTemplates() string
// 実行
func (t *Template) Execute(wr io.Writer, data any) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
メソッドチェーンを前提にした設計である。
tmpl := template.Must(
template.New("t").
Funcs(myFuncs).
Option("missingkey=error").
Delims("[[", "]]").
Parse(src),
)
Funcs / Option / Delims は Parse より前に呼ぶ必要がある。 Funcs は関数の存在確認がパース時に行われるため、後から足しても遅い。Delims も当然パース前でなければ意味がない。
8.2 Funcs — 拡張の唯一の入口
t.Funcs(template.FuncMap{
"upper": strings.ToUpper,
"repeat": strings.Repeat,
"fail": func() (string, error) { return "", fmt.Errorf("boom") },
})
関数の要件は次の通りである。
| 条件 | 内容 |
|---|---|
| 戻り値 | 1個、または (T, error) の2個 |
| 戻り値が0個 | ✗ |
| 戻り値が3個以上 | ✗ |
| 引数 | 任意個。可変長も可 |
| 名前 | 有効なGo識別子でなければ panic |
error を返すと Execute 全体が失敗する。
入力: {{ fail }}
出力: EXEC ERROR: template: t:1:3: executing "t" at <fail>: error calling fail: boom
この仕組みが「検証を関数として書く」パターンを可能にしている。 Helm の required、独自の mustBeOneOf のような関数がこれで実装される。
8.3 Option — missingkey の3モード
--- missingkey=default(既定)
map[string]string [<no value>]
map[string]int [<no value>]
--- missingkey=zero
map[string]string []
map[string]int [0]
--- missingkey=error
map[string]string [EXEC ERROR: ... map has no entry for key "nosuch"
map[string]int [EXEC ERROR: ... map has no entry for key "nosuch"
| 値 | 挙動 |
|---|---|
missingkey=default(= invalid、既定) | <no value> を出力 |
missingkey=zero | マップの要素型のゼロ値を出力 |
missingkey=error | 実行時エラーにする |
missingkey=zero の注意点 — マップが map[string]any の場合、any のゼロ値は nil なので <no value> が出る。
--- missingkey=zero、data が map[string]any の場合
[<no value>] ← zero を指定しても <no value> のまま
JSONを map[string]any に読んで渡す構成では missingkey=zero が効かない。 この構成は極めて一般的なので、実質的な選択肢は default か error になる。
設定ファイルを生成する用途では missingkey=error を強く推奨する。 <no value> が生成物に混入するより、生成が失敗するほうが安全である。
8.4 Delims — デリミタの変更
--- Delims を [[ ]] に変える
入力: [[ .Name ]] and {{ .Name }}
出力: Ada and {{ .Name }}
{{ }} は単なる既定値であり、変更できる。 変更すると元のデリミタは生テキストとして扱われる。
用途は「二重テンプレート」である。 テンプレートを生成するテンプレートを書くとき、外側と内側でデリミタを分ける。§10.6 と §10.10 で実例を扱う。
8.5 エラーの扱い
// 起動時に落とす(推奨)
var tmpl = template.Must(template.New("t").Parse(src))
// 実行時のエラーは必ず確認する
if err := tmpl.Execute(w, data); err != nil {
// 注意: この時点で w には途中まで書き込まれている
log.Printf("template execution failed: %v", err)
}
重要な注意点 — Execute がエラーを返しても、Writer には途中までの出力が書き込まれている。
入力: [{{ .Nope }}]
出力: [ ← "[" は既に書かれている
EXEC ERROR: ...
HTTPレスポンスに直接書くと、壊れた出力が送られる。 対策は bytes.Buffer に一度書いてから転送することである。
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
http.Error(w, "internal error", 500)
return
}
buf.WriteTo(w)
設定ファイル生成でも同じ配慮が必要である。 途中まで書かれたファイルが残ると、次の起動でそれを読んでしまう。テンポラリファイルに書いて rename するのが定石である。
9. html/template との違い
9.1 文脈依存エスケープ
html/template は text/template と同じAPIを持ちながら、出力先の文脈を解析して自動エスケープする。
data := map[string]any{
"Name": `<script>alert('xss')</script>`,
"URL": "javascript:alert(1)",
"Raw": template.HTML("<b>trusted</b>"),
}
src := `<p>{{ .Name }}</p>
<a href="{{ .URL }}">link</a>
<script>var n = {{ .Name }};</script>
raw: {{ .Raw }}`
=== text/template(エスケープなし)
<p><script>alert('xss')</script></p>
<a href="javascript:alert(1)">link</a>
<script>var n = <script>alert('xss')</script>;</script>
raw: <b>trusted</b>
=== html/template(文脈依存エスケープ)
<p><script>alert('xss')</script></p>
<a href="#ZgotmplZ">link</a>
<script>var n = "<script>alert('xss')</script>";</script>
raw: <b>trusted</b>
同じテンプレート、同じデータで結果が全く違う。
| 文脈 | html/template の処理 |
|---|---|
| HTML本文 | HTMLエンティティへエスケープ |
| 属性値 | 属性用のエスケープ |
href の値で危険なスキーム | #ZgotmplZ に置換 |
<script> 内 | JavaScript文字列リテラルとしてエスケープ(クォートも付与) |
template.HTML 型 | エスケープしない(明示的に安全と宣言した値) |
#ZgotmplZ は html/template 固有のマーカーである。 「Go template zero-gotcha」の意で、危険なURLを無害化した痕跡である。出力にこれが現れたら、テンプレートに javascript: や data: のURLが渡っている。
<script> 内でクォートが自動的に付く点にも注目したい。var n = {{ .Name }}; と書けば、値がJSON文字列リテラルになる。var n = "{{ .Name }}"; と自分でクォートを書くと二重になる。
9.2 text/template を HTML に使う危険
上の1行目がXSSそのものである。 text/template は <script> タグをそのまま出力する。
判断は単純である。
出力が HTML / ブラウザに解釈されるもの → html/template(例外なし)
それ以外(設定ファイル、ログ、SQL以外のテキスト) → text/template
「HTMLの一部だけを生成する」場合も html/template を使う。 部分だけなら安全という理屈は成立しない。
逆に、設定ファイル生成に html/template を使ってはいけない。 YAMLの中の & や < が勝手にエスケープされて壊れる。
9.3 使い分けの表
| 用途 | パッケージ |
|---|---|
| HTMLページ、HTMLメール | html/template |
| YAML / JSON / TOML / INI | text/template |
| シェルスクリプト | text/template(ただしシェルエスケープは自前で) |
| SQL | どちらも不可(プレースホルダを使う) |
| ログ、レポート、CLI出力 | text/template |
| Kubernetesマニフェスト | text/template |
SQLについて補足する。 テンプレートでSQLを組み立てるのはSQLインジェクションの温床である。database/sql のプレースホルダ(? / $1)を使う。テンプレートで組み立てるのは、どうしても動的にせざるを得ない識別子(テーブル名など)に限り、その場合もホワイトリスト検証を併用する。
10. 周辺ツールでの使われ方
10.1 なぜ text/template がデファクトになったのか
理由は3つある。
| 理由 | 内容 |
|---|---|
| 標準ライブラリである | 外部依存なし。Goで書かれたツールは無料で手に入る |
| チューリング完全でない | 設定生成に無限ループや任意コード実行を持ち込まない |
| 拡張が容易 | FuncMap に足すだけでドメイン固有機能が入る |
2つ目が特に重要である。 設定ファイル生成にJavaScriptやLuaを埋め込むと、テンプレートがプログラムになってしまう。text/template は「変数展開・条件・反復・関数呼び出し」に限定されているため、レビュー可能な範囲に留まる。
10.2 拡張の唯一の機構は FuncMap
周辺ツールがやっていることは、本質的に1つだけである。
t := template.New("x").Funcs(hugeFuncMap).Parse(src)
したがって「そのツール固有の関数リファレンス」を読めば、あとは text/template の知識で足りる。 逆に、コア言語の挙動({{- のトリム、and の戻り値、. の推移)はどのツールでも同一である。
この構造を実感するため、標準ライブラリだけで周辺ツールの代表的な関数を再実装して動かす。
var funcs = template.FuncMap{
// sprig: default — 空値なら既定値を返す(引数順が Go と逆)
"default": func(def, given any) any {
if isEmpty(given) { return def }
return given
},
// sprig: dig — ネストしたマップを安全に辿る。最後の引数が既定値
"dig": func(args ...any) any { /* ... */ },
"quote": func(s any) string { return fmt.Sprintf("%q", fmt.Sprint(s)) },
"toJSON": func(v any) string { b, _ := json.Marshal(v); return string(b) },
"base64Decode": func(s string) string { b, _ := base64.StdEncoding.DecodeString(s); return string(b) },
// Helm: nindent — 改行してから各行をインデント
"nindent": func(n int, s string) string { /* ... */ },
"indent": func(n int, s string) string { /* ... */ },
// Helm: required — 空なら実行を失敗させる
"required": func(msg string, v any) (any, error) {
if isEmpty(v) { return nil, fmt.Errorf("%s", msg) }
return v, nil
},
// consul-template: env / envOrDefault
"env": os.Getenv,
"envOrDefault": func(k, def string) string { /* ... */ },
}
--- default(引数順が逆)
{{ .Values.empty | default "fallback" }} / {{ default "fallback" .Values.image.repository }}
→ fallback / nginx
--- dig(安全なネスト参照)
{{ dig "image" "repository" "none" .Values }} / {{ dig "image" "tag" "latest" .Values }}
→ nginx / latest
--- quote / toJSON
{{ quote .Values.image.repository }} / {{ toJSON .Values.image }}
→ "nginx" / {"repository":"nginx"}
--- env / envOrDefault
{{ env "REGION" }} / {{ envOrDefault "NOPE" "us-west-2" }}
→ us-east-1 / us-west-2
--- Helm の nindent
spec:
data: {{ .block | nindent 4 }}
→ spec:
data:
line1
line2
--- Helm の indent
spec:
{{ .block | indent 2 }}
→ spec:
line1
line2
--- required で実行を止める
{{ required "image.tag is required" .Values.nosuch }}
→ EXEC ERROR: template: t:1:3: executing "t" at <required ...>: error calling required: image.tag is required
default の引数順が「既定値が先」である理由が、これで分かる。 パイプラインで | default "x" と書くと、パイプの左辺が最後の引数になる(§6.1)。だから既定値を第1引数に置く設計になっている。
required が実行を止められるのは、テンプレート関数が error を返せるからである(§8.2)。標準ライブラリの仕様をそのまま利用している。
10.3 sprig — 事実上の標準関数ライブラリ
組み込み関数が19個しかない(§6.2)ため、実用的なテンプレートには関数ライブラリが必要になる。 その共通実装が sprig(github.com/Masterminds/sprig)である。
sprig を採用しているツール:
Helm / Nomad(一部)/ nomad-pack / consul-template(sprig_ 接頭辞つき)
Kubernetes の各種オペレータ / Argo Workflows / kustomize プラグイン など
sprig の関数群は分野別に整理されている。
| 分野 | 代表的な関数 |
|---|---|
| 文字列 | upper, lower, trim, trimPrefix, replace, split, join, contains, hasPrefix, repeat, substr, nospace, title, camelcase, snakecase |
| 既定値 | default, empty, coalesce, ternary, required(Helm拡張) |
| 型変換 | toString, toJson, toPrettyJson, toYaml(Helm拡張), atoi, float64, int |
| リスト | list, first, last, rest, initial, append, prepend, concat, uniq, without, has, compact, sortAlpha, reverse |
| 辞書 | dict, get, set, unset, hasKey, keys, values, pick, omit, merge, mergeOverwrite, deepCopy, dig |
| 算術 | add, sub, mul, div, mod, max, min, ceil, floor, round |
| 暗号 | sha256sum, sha1sum, adler32sum, bcrypt, htpasswd, genPrivateKey, genCA, genSelfSignedCert |
| エンコード | b64enc, b64dec, b32enc, b32dec |
| 日時 | now, date, dateModify, toDate, unixEpoch |
| フロー | fail, regexMatch, regexReplaceAll, regexFind |
| その他 | uuidv4, semver, semverCompare, env, expandenv |
注意すべき点が2つある。
① sprig の一部の関数は非決定的である。 now、uuidv4、randAlphaNum を使うと、実行するたびに出力が変わる。生成物をGitにコミットする運用(GitOps)では避けるべきである。 Helm も helm template の冪等性が壊れるため、これらの使用を推奨していない。
② env / expandenv は環境変数を読む。 Helm では意図的に無効化されている(チャートが実行環境の環境変数に依存すると再現性が失われるため)。ツールによって有効・無効が違う。
10.4 Helm
Helm のテンプレートは text/template + sprig + Helm独自関数である。
# templates/deployment.yaml(典型的な形)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount | default 1 }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
Helm独自の関数・値:
| 名前 | 内容 |
|---|---|
include "name" . | template の代替。 結果を文字列として返すのでパイプできる |
tpl "文字列" . | 文字列をテンプレートとして評価する(動的テンプレート) |
required "msg" .Values.x | 空なら失敗させる |
toYaml / fromYaml | YAML変換 |
lookup | クラスタの既存リソースを参照(helm template では nil) |
.Values | values.yaml の内容 |
.Chart | Chart.yaml の内容 |
.Release | リリース名、namespace など |
.Capabilities | クラスタのバージョンやAPI一覧 |
.Files | チャート内のファイル |
include と template の違いが最重要である。
{{ template "x" . }} → 出力に直接書く。パイプできない
{{ include "x" . }} → 文字列を返す。パイプできる
{{ include "x" . | nindent 4 }} → だからインデントを調整できる
template は text/template の組み込みアクションであり、値を返さない。 そのため Helm は「文字列を返す include」を関数として追加した。YAMLのインデント調整に必須であるため、Helmチャートでは template よりも include が使われる。
nindent が Helm チャートに頻出する理由も同じである。
{{- include "mychart.labels" . | nindent 4 }}
{{- で直前の改行を消す
include で部分テンプレートを文字列として取得
nindent 4 で「改行 + 各行を4スペースインデント」する
この3点セットが Helm チャートの定型句である。 {{- を書かないと空行が入り、nindent を使わないとインデントが崩れる。
10.5 consul-template
consul-template は Consul / Vault / ファイル / 環境変数を参照する関数を追加している。 HashiCorp のツール群(Nomad、Consul、Vault)の設定配布の中核である。
実際のテンプレートを見る。 次は Vault のシークレットを .properties 形式へ展開するテンプレートの構造である。
# rendered by serenity
# template used: {{ env "TEMPLATE_FILE" }}
# data file: {{ env "CONFIG_FILE" }}
# config version: {{ env "CONFIG_VERSION" }}
# generated at: {{ timestamp "Mon Jan 2 15:04:05 -0700 MST 2006" }}
{{ $config_file := env "CONFIG_FILE" -}}
{{- $mountBaseDir := envOrDefault "MOUNT_BASE_DIR" "" -}}
{{- with $config := file $config_file | parseJSON -}}
{{- range $key, $value := $config.properties }}
{{ $key }}={{ $value | toJSON | trim "\"" }}
{{- end }}
{{- range $key, $value := $config.secrets }}
{{- if $value.mountPath }}
{{- with secret (printf "%s/%s" $value.path (sprig_default $value.version -1)) }}
{{- .Data.data.secret | base64Decode | writeToFile (printf "%s%s" $mountBaseDir $value.mountPath) "" "" "0644" }}
{{- end }}
{{- else }}
{{ $key }}={{ with secret $value.path }}{{ .Data.data.secret }}{{ end }}
{{- end }}
{{- end }}
{{- end }}
この1つのテンプレートに、本ガイドで扱った要素がほぼすべて現れている。
| 使われている機能 | 節 |
|---|---|
{{- / -}} によるトリム(全行) | §5.4 |
$config_file := env "..." 変数宣言 | §4.3 |
with $config := ... 変数束縛つき with | §5.3 |
range $key, $value := $config.properties マップの反復 | §5.2 |
if $value.mountPath — マップのキー有無で分岐 | §5.1, §3.4 |
$value | toJSON | trim "\"" パイプの連鎖 | §6.1 |
printf "%s/%s" ... 引数の組み立て | §6.5 |
with secret ... — 取得できたときだけ実行(nilガード) | §5.3 |
consul-template が追加している関数:
| 関数 | 内容 |
|---|---|
secret "path" | Vault のシークレットを取得(.Data.data.x で値) |
secrets "path" | Vault のパス一覧 |
key "path" | Consul KV の値 |
keyOrDefault | 同、既定値つき |
ls / tree | Consul KV の一覧・木 |
service "name" | Consul のサービス一覧(ヘルスチェック通過分) |
nodes / datacenters | Consul のノード・DC |
file "path" | ファイルの内容 |
env / envOrDefault | 環境変数 |
parseJSON / parseYAML / parseTOML | 文字列をパース |
toJSON / toYAML / toTOML | 構造をシリアライズ |
writeToFile | ファイルへ書き出す(副作用つき関数) |
timestamp | 現在時刻 |
base64Decode / base64Encode | Base64 |
sprig_* | sprig の全関数(接頭辞つき) |
注目点が3つある。
① sprig_ 接頭辞。 consul-template は sprig を取り込むが、自前の関数名と衝突しないよう sprig_default、sprig_dig のように接頭辞を付けている。同じ default がツールによって default か sprig_default かに分かれるため、テンプレートの移植時に注意が必要である。
② writeToFile は副作用を持つ。 テンプレート関数がファイルを書くのは text/template の設計思想からは外れるが、「複数のファイルを1回のレンダリングで生成する」ために使われている。上のテンプレートでは、証明書のような大きなバイナリを本文に出さず別ファイルへ書き出している。
③ with secret の nil ガードが本質的である。 シークレットが取得できなかった場合、with の中身がスキップされる。これにより「Vault が一時的に応答しない」ときに空の値を書き込む事故を避けている。
10.6 nomad-pack — デリミタを変える
nomad-pack は Nomad のジョブ仕様そのものをテンプレートから生成するツールである。 そしてデリミタを [[ ]] に変えている。
[[ define "serenityTask" ]]
task "serenity" {
driver = "docker"
lifecycle {
hook = "prestart"
sidecar = false
}
config {
image = "[[ (var "serenity_image" $) ]]:[[ (var "serenity_tag" $) ]]"
force_pull = [[ (var "serenity_force_pull" $) ]]
command = "[[ (var "serenity_command" $) ]]"
}
env {
VAULT_ADDR = [[ (var "serenity_vault_addr" $) | quote]]
CONFIG_FILE = "/alloc/data/[[ (var "serenity_config_environment" $) ]].json"
}
}
[[ end -]]
なぜ [[ ]} にするのか。 生成先である Nomad のジョブ仕様には template スタンザがあり、その中身は Nomad 自身が consul-template({{ }})で評価する。もし nomad-pack が {{ }} を使うと、Nomad へ渡す前に展開されてしまう。
[[ ]] → nomad-pack がレンダリング時に評価する
{{ }} → Nomad が実行時に評価する
両者が同じファイルに共存する。
[[ if (var "splunk_hec" $) ]]
template {
data = <<EOF
SPLUNK_HEC_TOKEN="{{ with secret "secrets/hse/d2p/[[ (var "vault_region" $) ]]/splunk/secrets/hec-token" }}{{ .Data.data.secret }}{{ end }}"
EOF
destination = "secrets/splunk.env"
env = true
}
[[- end ]]
[[ (var "vault_region" $) ]] が Vault のパスの一部を組み立て、外側の {{ with secret ... }} はジョブ仕様として残る。 二重テンプレートを、デリミタの分離だけで成立させている。§10.10 で改めて論じる。
nomad-pack が使っている sprig 関数の例:
deepCopy マップを破壊せず複製する
mergeOverwrite 深いマージ(後が勝つ)
dig ネストしたキーを安全に取得
default 空値なら既定値
quote HCL の文字列リテラル化
toStringList HCL のリストリテラル化
contains 部分文字列判定
upper / replace / get
deepCopy + mergeOverwrite の組み合わせで「既定値の継承」を実装している。
[[- $default := deepCopy (var "task_group_defaults" $) | mergeOverwrite (var "task_group" $.defaults) -]]
deepCopy が必要な理由は、mergeOverwrite が第1引数を破壊するためである。 ループの各周回で $default が汚染されると、1つ目のポッドの設定が2つ目に漏れる。
そして値の解決には dig を使う。
resources {
cpu = [[ dig "resources" "cpu" $default.tasks.resources.cpu $task ]]
memory = [[ dig "resources" "memory" $default.tasks.resources.memory $task ]]
}
「$task にあればそれ、無ければ $default」という4段の優先順位を、この1関数で表現している。
10.7 Nomad の template スタンザ
Nomad のジョブ仕様に直接テンプレートを書ける。 中身は consul-template である。
task "app" {
template {
data = <<EOF
{{ with secret "secret/data/myapp/config" }}
DB_PASSWORD={{ .Data.data.password }}
API_KEY={{ .Data.data.api_key }}
{{ end }}
{{ range service "redis" }}
REDIS_ADDR={{ .Address }}:{{ .Port }}
{{ end }}
EOF
destination = "secrets/app.env"
env = true
change_mode = "restart"
}
}
| 属性 | 内容 |
|---|---|
data | テンプレート本文(インライン) |
source | テンプレートファイルのパス(data の代替) |
destination | 出力先 |
env | 真なら生成物を環境変数として読み込む |
change_mode | 値が変わったときの動作(restart / signal / noop) |
perms | 出力ファイルのパーミッション |
change_mode が consul-template の常駐モードと連動している。 Vault のシークレットがローテートされると、テンプレートが再レンダリングされ、restart ならタスクが再起動する。「設定が変わったらプロセスを入れ替える」という運用がテンプレートエンジンの機能として提供されている。
10.8 kubectl -o go-template
kubectl は API レスポンスに対して text/template を適用できる。
$ kubectl create -f pod.yaml --dry-run=client \
-o go-template='{{ .metadata.name }}: {{ range .spec.containers }}{{ .name }}={{ .image }} {{ end }}{{ "\n" }}'
demo: c1=nginx:1.27 c2=redis:7
$ kubectl create -f pod.yaml --dry-run=client \
-o go-template='{{ range $i, $c := .spec.containers }}{{ $i }} {{ $c.image }}{{ "\n" }}{{ end }}'
0 nginx:1.27
1 redis:7
$ kubectl create -f pod.yaml --dry-run=client \
-o go-template='{{ range $k, $v := .metadata.labels }}{{ $k }}={{ $v }};{{ end }}{{ "\n" }}'
app=web;tier=front;
注意点が3つある。
① 改行は {{ "\n" }} で出す。 シェルの引数として1行で書くため、テンプレート内に実際の改行を入れづらい。
② フィールド名は API の JSON 名(小文字始まり)である。 Go の構造体フィールド名ではない。.metadata.name であって .Metadata.Name ではない。
③ ラベルの map は自動的にキーでソートされる(§5.2)。上の出力が app → tier の順になっているのはそのためである。kubectl の出力が安定するのはこの性質のおかげである。
-o jsonpath との使い分け:
-o go-template | -o jsonpath | |
|---|---|---|
| 表現力 | 高い(条件分岐・関数・変数) | 低い(パス指定のみ) |
| 簡潔さ | 低い | 高い |
| 学習コスト | text/template の知識が必要 | JSONPath の知識 |
| 適する場面 | 整形が必要、条件で出し分ける | 単に値を1つ抜く |
# 単に値を抜くなら jsonpath が短い
$ kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'
# 条件や整形が要るなら go-template
$ kubectl get pods -o go-template='{{ range .items }}{{ if eq .status.phase "Running" }}{{ .metadata.name }}{{ "\n" }}{{ end }}{{ end }}'
-o go-template-file=x.tmpl でファイルから読める。 複雑なものはファイルにする。
10.9 Docker、Prometheus、その他
Docker の --format:
$ docker version --format '{{ .Client.Version }} / {{ .Client.Os }}/{{ .Client.Arch }}'
29.2.0 / darwin/arm64
$ docker version --format '{{ json .Client.Version }}'
"29.2.0"
$ docker version --format '{{ .Nope }}'
template: version:1:3: executing "version" at <.Nope>: can't evaluate field Nope in type system.versionInfo
Docker は json 関数を追加している。 エラーの形は素の text/template とまったく同じである(can't evaluate field ... in type ...)。
# よく使う形
$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
$ docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}'
$ docker inspect --format '{{ range .Mounts }}{{ .Source }}→{{ .Destination }}{{ "\n" }}{{ end }}' <container>
table 接頭辞は Docker 独自の拡張である(ヘッダ行を付けて列を揃える)。
Prometheus / Alertmanager:
{{ define "slack.title" }}[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}{{ end }}
{{ define "slack.text" }}
{{ range .Alerts }}
*Alert:* {{ .Annotations.summary }}
*Severity:* {{ .Labels.severity }}
*Started:* {{ .StartsAt.Format "2006-01-02 15:04:05" }}
{{ end }}
{{ end }}
Alertmanager は toUpper、title、match、reReplaceAll、safeHtml などを追加している。 .StartsAt.Format のようにメソッドを直接呼べる点は text/template の標準機能である(§3.5)。
Prometheus のコンソールテンプレートも同じ仕組みで、query、first、value、label などの関数が追加されている。
その他の主なツール:
| ツール | 用途 |
|---|---|
| Grafana Loki / Promtail | template ステージでラベル加工 |
| Argo Workflows / Argo CD | マニフェストの生成 |
| Terraform(provider 側) | 一部プロバイダのテンプレート機能 |
| Vault | ACLポリシーテンプレート、レスポンスラッピング |
| Cosign / Notary | 出力整形 |
| gomplate | 汎用テンプレートCLI(sprig + 多数のデータソース) |
| Hugo | 静的サイト生成(大幅に拡張された独自関数群) |
gomplate は「text/template をシェルから使う」ためのツールである。 CIで設定ファイルを生成する用途に便利で、envsubst の高機能な代替として使える。
10.10 二重テンプレートの問題
周辺ツールを扱うときに最も厄介なのが「テンプレートを生成するテンプレート」である。
① nomad-pack が [[ ]] でジョブ仕様を生成する
② そのジョブ仕様には {{ }} の consul-template が含まれる
③ Nomad が実行時に {{ }} を評価する
この3段構成では、どのデリミタがいつ評価されるかを常に意識する必要がある。
対処法は3つある。
① デリミタを分ける(推奨)
t.Delims("[[", "]]")
nomad-pack がこれを採用している。最も明快で、混乱が起きない。
② エスケープする
デリミタを変えられない場合、リテラルとして出力する。
{{ "{{" }} .Value {{ "}}" }}
$ cat > esc.tmpl <<'X'
{{ "{{" }} .Inner {{ "}}" }}
X
出力: {{ .Inner }}
{{ "{{" }} は「文字列リテラル {{ を出力する」という意味である。 読みにくいが確実に動く。
③ printf を使う
{{ printf "{{ .Inner }}" }}
より読みやすい。 ただし printf の書式指定子(%)が含まれる場合はエスケープが必要になる。
デバッグの原則:
生成の各段で「その時点の中間出力」を確認する。
nomad-pack render → 生成されたジョブ仕様を目で見る
→ {{ }} が残っていることを確認する
Nomad へ投入 → alloc のログで最終的な設定を見る
中間出力を確認せずに最終結果だけを見ると、どの段で壊れたか分からない。 Helm でも helm template で中間出力を見るのが基本である。
11. 落とし穴一覧
本節の挙動はすべて Go 1.26.7 で再現したものである。
| # | 落とし穴 | 症状 | 対処 | 節 |
|---|---|---|---|---|
| 1 | map の欠損キー | <no value> が生成物に混入する | index を使う / missingkey=error | 3.4 |
| 2 | nil ポインタの出力 | <nil> が混入する | with でガードする | 3.4 |
| 3 | struct の欠損フィールド | 実行時エラー | 綴りを直す | 3.4 |
| 4 | and / or が値を返す | {{ and .A .B }} が .B の値を出す | 条件としてのみ使う。not (not x) で真偽値化 | 6.3 |
| 5 | min / max が無い | function "min" not defined | sprig を使う / 自前で追加 | 6.2 |
| 6 | 文字列関数が1つも無い | function "upper" not defined | sprig を使う | 6.2 |
| 7 | eq の型不一致 | incompatible types for comparison | printf "%v" で揃える | 6.6 |
| 8 | print の空白規則 | 意図しない空白が入る | printf "%s%s" を使う | 6.5 |
| 9 | {{- が空白を全部食う | インデントが崩れる | 消す範囲を意識する | 5.4 |
| 10 | {{-if}} は不正 | bad number syntax: "-if" | {{- if}} と空白を入れる | 5.4 |
| 11 | ループ内 := が外に漏れない | カウンタが0のまま | = で代入する | 4.4 |
| 12 | template に引数を渡し忘れる | . が nil になる | 末尾に . を書く | 4.1 |
| 13 | template は値を返さない | パイプできない | Helm なら include を使う | 10.4 |
| 14 | 空の define は上書きしない | ブロックを消せない | 空白以外を置く | 7.4 |
| 15 | 同一 Parse 内の重複 define | multiple definition of template | 分けて書く | 7.4 |
| 16 | Clone 忘れ | 全ページが最後のテンプレートになる | Clone() してから Parse | 7.3 |
| 17 | New(name).ParseFiles() | "name" is an incomplete or empty template | New せず ParseFiles / ExecuteTemplate | 7.5 |
| 18 | 未定義テンプレート呼び出し | 実行時まで気づかない | テスト網羅 / Lookup で事前確認 | 2.3 |
| 19 | Templates() の順序が不定 | 一覧表示が毎回変わる | 自分でソートする | 7.5 |
| 20 | エラー時に途中まで出力される | 壊れた出力が送られる | bytes.Buffer 経由にする | 8.5 |
| 21 | パースを毎回やる | 約5倍遅い | パッケージ変数に持つ | 2.4 |
| 22 | text/template で HTML | XSS | html/template を使う | 9.2 |
| 23 | html/template で YAML | & < が壊れる | text/template を使う | 9.3 |
| 24 | missingkey=zero が効かない | map[string]any では <no value> | error にする | 8.3 |
| 25 | ポインタメソッドが呼べない | can't evaluate field X | データをポインタで渡す | 3.5 |
| 26 | call に関数名を渡す | wrong number of args | データ内の関数値に使う | 6.7 |
| 27 | 二重テンプレートの混同 | 早すぎる展開 | Delims で分ける | 10.10 |
| 28 | sprig の非決定的関数 | GitOps で毎回差分 | now / uuidv4 を避ける | 10.3 |
11.1 とくに危険な3つ
この3つは「静かに壊れる」ため、優先度が高い。
① <no value> / <nil> の混入(#1, #2)
エラーにならないため、生成物が壊れたまま下流へ流れる。
$ cue export ... > out.yaml # 例: 設定生成
$ grep -n '<no value>\|<nil>' out.yaml # ← CI に入れる価値がある
生成物に対する grep をCIに入れるだけで防げる。
② and / or を出力に使う(#4)
{{ and .A .B }} → true ではなく .B の値が出る
条件文以外で使わないという規律で防ぐ。
③ エラー時の部分出力(#20)
入力: [{{ .Nope }}]
出力: [ ← ここまで書かれている
EXEC ERROR: ...
bytes.Buffer に書いてから転送するという定型で防ぐ。設定ファイルなら一時ファイル + rename にする。
12. まとめ
12.1 1枚にまとめる
text/template の本質 = 「テキスト + アクション」+「reflect でデータを辿る」
{{ }} の外はそのまま出力、内はアクション
. は「現在の値」で、range / with / template が差し替える
$ は常にルート
組み込み関数は19個だけ
and or not / eq ne lt le gt ge / print printf println
index slice len / call / html js urlquery
→ 文字列操作も算術も無い
→ だから FuncMap による拡張が必須で、sprig が事実上の標準になった
and / or は真偽値ではなく「値」を返す(1.18 以降は短絡評価)
参照失敗の挙動が3通りに分かれる
struct の欠損フィールド → 実行時エラー
map の欠損キー → <no value> を静かに出力 ★危険
nil ポインタ → 参照でエラー、出力で <nil> ★危険
パース時に落ちるもの: 構文、関数名
実行時に落ちるもの: フィールド、テンプレート名、型
{{- と -}} は「連続する空白すべて」を消す
→ YAML 生成では生死を分ける
12.2 学習の順序
| 順序 | 学ぶこと | 理由 |
|---|---|---|
| 1 | . の推移(range / with / template) | 誤解の最大の源 |
| 2 | 空白制御 {{- -}} | 生成物の見た目を左右する |
| 3 | $ と変数のスコープ | ループ内の値の持ち出しに必要 |
| 4 | パイプラインと「最後の引数」規則 | sprig の引数順の理解に必要 |
| 5 | 参照失敗の3通りの挙動 | 静かな障害を防ぐ |
| 6 | define / template / block | テンプレートの再利用 |
| 7 | パース時 vs 実行時エラー | デバッグの方向を決める |
| 8 | Go 側の API(Funcs / Option / Clone) | 自分でツールを作るとき |
| 9 | 周辺ツールの独自関数 | 必要になった時点で |
1〜5を理解すれば、Helm や consul-template のテンプレートはほぼ読める。 残りはツール固有の関数リファレンスを引くだけになる。
12.3 実務での使いどころ
適している:
| 用途 | 理由 |
|---|---|
| 設定ファイルの生成 | 標準ライブラリで完結し、チューリング完全でない |
| CLIツールの出力整形 | ユーザーが書式を指定できる(--format) |
| コード生成 | go:generate と組み合わせやすい |
| 通知メッセージの整形 | Alertmanager 方式 |
| 単一バイナリへの埋め込み | embed + ParseFS |
適していない:
| 用途 | 代替 |
|---|---|
| 複雑な変換ロジック | Goのコードで組み立ててからテンプレートに渡す |
| 構造の妥当性が重要な設定 | CUE / Jsonnet(構造を理解する言語) |
| HTML | html/template(同じAPIなので移行は容易) |
| SQL | プレースホルダ |
最後の「構造の妥当性」について補足する。 text/template は文字列を組み立てているだけで、出力がYAMLとして妥当かを知らない。インデント1つで壊れる。構造そのものを扱いたいなら、CUE や Jsonnet のように「値」を組み立てる言語のほうが適している。 一方で、既存のエコシステム(Helmチャートの豊富さ)を捨てられないという現実もある。
折衷案としては、values.yaml を構造を理解する言語で生成・検証し、チャート自体は Helm のまま使う構成がある。
$ cue export -e 'helmValues' --out yaml > values.yaml
$ helm upgrade --install myapp ./chart -f values.yaml
13. 参考情報
| 項目 | 場所 |
|---|---|
text/template パッケージドキュメント | https://pkg.go.dev/text/template |
html/template | https://pkg.go.dev/html/template |
text/template/parse(AST) | https://pkg.go.dev/text/template/parse |
| ソース(組み込み関数の定義) | $(go env GOROOT)/src/text/template/funcs.go |
| sprig | https://masterminds.github.io/sprig/ |
| Helm テンプレートガイド | https://helm.sh/docs/chart_template_guide/ |
| Helm 独自関数 | https://helm.sh/docs/chart_template_guide/function_list/ |
| consul-template | https://github.com/hashicorp/consul-template |
| nomad-pack | https://github.com/hashicorp/nomad-pack |
| Nomad の template スタンザ | https://developer.hashicorp.com/nomad/docs/job-specification/template |
| kubectl の出力形式 | https://kubernetes.io/docs/reference/kubectl/#output-options |
| Docker の format | https://docs.docker.com/engine/cli/formatting/ |
| Alertmanager の通知テンプレート | https://prometheus.io/docs/alerting/latest/notifications/ |
| gomplate | https://docs.gomplate.ca/ |
検証環境と検証方法について
$ go version
go version go1.26.7 darwin/arm64
$ sw_vers -productVersion
15.x # Apple M4 Max
本ガイドに掲載した内容のうち、検証の度合いは次の通りである。
| 対象 | 検証方法 |
|---|---|
| §2〜§9 のテンプレート・Goコード・出力・エラー | すべて Go 1.26.7 で実行した実測値 |
| §6.2 の組み込み関数一覧 | funcs.go のソースから抽出 |
| §2.4 のベンチマーク | go test -bench の実測値 |
| §10.2 の関数再実装のデモ | 標準ライブラリのみで実装し実行した実測値 |
§10.8 の kubectl 出力 | kubectl で実行した実測値 |
§10.9 の docker 出力 | docker で実行した実測値 |
| §10.3 の sprig 関数一覧 | 未実行(proxy.golang.org に到達できない環境のため、公式ドキュメントに基づく記載) |
| §10.4〜§10.7 のツール固有テンプレート | 実在するテンプレートの構造に基づく記載(当該ツールを動かした出力ではない) |
sprig と各ツールの関数一覧については実行検証をしていない。 関数名や引数順はバージョンによって変わりうるため、実際に使う際は当該ツールのバージョンに対応するドキュメントを確認することを推奨する。一方で、§10.2 で示した「拡張の機構」そのものは標準ライブラリだけで再現・実行して確認している。