フューチャー技術ブログ

Go 1.23リリース連載 text/template

はじめに

TIGの辻です。Go1.23連載の5本目です。
この記事では、マイナーアップデートから text/template パッケージを取り上げて紹介します。

text/template のアップデート内容

  • テンプレートが新しいアクション “else with” をサポートするようになりました(#57646

テンプレートが新しいアクション “else with” をサポートするようになりました(#57646

text/template はテンプレートを用意し、データをテンプレートの値として当てはめて、テキスト出力できる便利なパッケージです。

FutureのいくつかのOSSでも、この text/template パッケージを利用しています。たとえば future-architect/vuls があります。

text/template パッケージで利用できる以下のようなテンプレートを用意し、このテンプレートにデータを当てはめて、結果の文字列を取得できます。

const mdTemplate = `
{{.CveID}}
================

CVSS Scores
-----------
{{.Cvsses }}

{{if .SSVC}}
SSVC
-----------
{{range $ssvc := .SSVC -}}
* {{$ssvc.Type}}
Exploitation : {{$ssvc.Value.Exploitation}}
Automatable : {{$ssvc.Value.Automatable}}
TechnicalImpact : {{$ssvc.Value.TechnicalImpact}}
{{end}}
{{end}}

Summary
-----------
{{.Summary }}

Mitigation
-----------
{{.Mitigation }}

Primary Src
-----------
{{range $link := .Links -}}
* {{$link}}
{{end}}
Patch
-----------
{{range $url := .PatchURLs -}}
* {{$url}}
{{end}}
CWE
-----------
{{range .Cwes -}}
* {{.En.CweID}} [{{.En.Name}}](https://cwe.mitre.org/data/definitions/{{.En.CweID}}.html)
{{end}}
{{range $name := .CpeURIs -}}
* {{$name}}
{{end}}
Confidence
-----------
{{range $confidence := .Confidences -}}
* {{$confidence.Score}} / {{$confidence.DetectionMethod}}
{{end}}
References
-----------
{{range .References -}}
* [{{.Source}}]({{.Link}})
{{end}}

`

https://github.com/future-architect/vuls/blob/f31cf230ab368df433a6dd5ceb8d8c6b9678585d/tui/tui.go#L1024-L1080

さて、テンプレート内でデータを評価するために利用できるアクション(≒構文)がいくつかあります。ifrange with break などといったものです。今回のアップデートでは with に関するアクション else with が追加になりました。

withアクションとは

そもそも with のアクションを利用したことがない方も多いと思いますので、本記事で簡単に触れておきます。ちなみに私も今回の記事を書くまではテンプレートで with というアクションがあることを知りませんでした。

ドキュメントには以下のように記載があります。pipeline が空であれば、出力は生成されません。空でない場合はドットに pipeline の結果がセットされて、T1 が実行されます。

{{with pipeline}} T1 {{end}}
If the value of the pipeline is empty, no output is generated;
otherwise, dot is set to the value of the pipeline and T1 is
executed.

テンプレートで条件分岐したい場合 if を利用すれば十分である場合が多そうですが with が有用である例として、人物名に対応するフルーツ名をテンプレート内で取得するような、次のテンプレートを考えてみます。

package main

import (
"log"
"os"
"text/template"
)

// getMatchFruits() の関数が実行されて引数の名前に対応するフルーツを取得します
// . に結果が格納されているので、その値を出力するテンプレートです
const sampleTemplate = `
{{with getMatchFruits .Name}}
{{ . }}
{{else}}
nothing
{{end}}
`

type User struct {
Name string
}

func getMatchFruits(person string) string {
m := map[string]string{
"alice": "apple",
"bob": "banana",
"chuck": "cherry",
}
return m[person]
}

func main() {
funcMap := template.FuncMap{
"getMatchFruits": getMatchFruits,
}

tpl, err := template.New("sample").Funcs(funcMap).Parse(sampleTemplate)
if err != nil {
log.Fatal(err)
}

a := User{Name: "alice"}
if err := tpl.Execute(os.Stdout, a); err != nil {
log.Fatal(err)
}
// 出力結果
// apple

t := User{Name: "tom"}
if err := tpl.Execute(os.Stdout, t); err != nil {
log.Fatal(err)
}
// 出力結果
// nothing
}

テンプレート内で関数を評価をして、その結果を . で参照できるため、テンプレート内のコードがシンプルになります。もちろん with ではなく if を利用しても以下のように書けますが、若干テンプレートのコードが冗長になります。

テンプレートのアクションで if を利用する場合
const sampleTemplate = `
-{{with getMatchFruits .Name}}
+{{if getMatchFruits .Name}}
- {{ . }}
+ {{ getMatchFruits .Name }}
{{else}}
nothing
{{end}}
`
...

Go 1.22までのwith

さて Go 1.22 までは with を利用したアクションは以下の2種類でした。

  • {{with pipeline}} T1 {{end}}
  • {{with pipeline}} T1 {{else}} T0 {{end}}

今回のアップデートは、上記の2つのアクションに加えて else with として分岐できるようにしたい、ということです。
if を利用したアクションでは以下の3種類ありますが with の場合は2種類で else with は存在しませんでした。

  • {{if pipeline}} T1 {{end}}
  • {{if pipeline}} T1 {{else}} T0 {{end}}
  • {{if pipeline}} T1 {{else if pipeline}} T0 {{end}}

この機能追加があると何が嬉しい?

例で紹介したように、テンプレート内で複雑な条件分岐している場合に、コードがシンプルになります。

#57646 のProposalを上げている willfaught さんはGoの静的サイトジェネレータであるHugoのテーマ Paige を開発されている方です。Goのテンプレートを駆使しており、実際のテンプレートのコードとして以下があったようです。

{{ with $page.Resources.GetMatch $url }}
{{ $result = . }}
{{ else }}
{{ with $page.Resources.Get $url }}
{{ $result = . }}
{{ else }}
{{ with resources.GetMatch $url }}
{{ $result = . }}
{{ else }}
{{ with resources.Get $url }}
{{ $result = . }}
{{ else }}
{{ with resources.GetRemote $url }}
{{ $result = . }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}
{{ end }}

現実問題として、こういうコードを書かざるを得ないが、困っている、というのは Issue としてインパクトがあります。pipeline に関数を利用しており、変数 $url を関数に渡して、結果が存在すればその結果を $result 変数に値を格納しています。こういったケースでは if ではなく with が有用と感じますが Go 1.22 の構文だけですとネストが深くなってしまい、テンプレートのコードが複雑になります。

with else が導入されると上記のテンプレートコードは以下のように書き直せます。シンプルです。#57646 に記載があるこのようなユースケースを考慮して、機能追加は問題ない、としてGo1.23でリリースとなりました。

{{ with $page.Resources.GetMatch $url }}
{{ $result = . }}
{{ else with $page.Resources.Get $url }}
{{ $result = . }}
{{ else with resources.GetMatch $url }}
{{ $result = . }}
{{ else with resources.Get $url }}
{{ $result = . }}
{{ else with resources.GetRemote $url }}
{{ $result = . }}
{{ end }}

ちなみに else with が利用できない場合、上記のテンプレートのコードは if を利用して次のように記述できるでしょう。

{{ if $page.Resources.GetMatch $url }}
{{ $result = $page.Resources.GetMatch $url }}
{{ else if $page.Resources.Get $url }}
{{ $result = $page.Resources.Get $url }}
{{ else if resources.GetMatch $url }}
{{ $result = resources.GetMatch $url }}
{{ else if resources.Get $url }}
{{ $result = resources.Get $url }}
{{ else if resources.GetRemote $url }}
{{ $result = resources.GetRemote $url }}
{{ end }}

おわりに

text/template のアップデートについて触れてみました。

with を利用しているテンプレートで複雑な条件分岐が必要なケースでは、今回のシンタックスシュガーの導入でコードがシンプルに記述できるようになるでしょう。今回のUpdateでは text/template パッケージを利用したテンプレートが複雑である場合に特に嬉しいアップデートと感じました。

次は市川さんのos.CopyFS & path/filepathです。