Go Template の基本的な動作を見てみよう vol.2


はじめに

前回投稿した「Go Template の基本的な動作を見てみよう」の続きです。

https://qiita.com/_otakakot_/items/3ca4eba2576bc5bb1f30

前回の記事で Go Template に与えた値がどのように展開(評価)されるのかを確認してみました。 今回の記事は、Actionsにおける制御構文について挙動を見ていこうと思います。

※ 本記事ではtext/templateを用いて説明しますが、HTMLを出力する場合はコード・インジェクションに対応しているhtml/templateを用いる必要があります。

おさらい(基本文法)

公式ドキュメントに記載されているコードをもとに Go Template の基本文法を振り返ります。

package main

import (
	"html/template"
	"os"
)

// 0. テンプレート内部で展開したい構造体(型)を定義
type Inventory struct {
	Material string
	Count    uint
}

func main() {
    // 1. テンプレート内部で展開したい値を指定
	sweaters := Inventory{"wool", 17}

    // 2. テンプレート文字列を定義
	tmpl, err := template.New("test").Parse("{{.Count}} items are made of {{.Material}}")

	if err != nil {
		panic(err)
	}

    // 3. 展開および出力
	err = tmpl.Execute(os.Stdout, sweaters)
	if err != nil {
		panic(err)
	}
}

上記の流れを追うことで Go Template は利用することができます。

Actionsとは?

公式ドキュメントのoverviewに以下の記載があります。

“Actions”—data evaluations or control structures—

データ評価または制御構文のことを示します。 前回の記事で解説したGo Template の基本的な動作を見てみようはデータ評価であるためActionsであったと言えます。 今回はActionsの中でも「制御構文」に着目した内容となります。

比較演算子

Actionsの説明に入るまえに、Go Template での比較演算子ついて確認しておきます。 Go Template では以下の比較演算子が用意されていて利用することができます。

  • eq (==): equal
  • ne (!=): not equal
  • lt (< ): less than
  • le (<=): less than or equal
  • gt (> ): greater than
  • ge (>=): greater than or equal

比較演算子はFunctionsに分類される構文であり {{ eq .Arg1 .Arg2 }} のように記述します。 <比較演算子> <第一引数> <第二引数>となります。ltgtなどの場合は第一引数は第二引数より小さい(大きい)と考えるとよさそうです。

実際のコードは以下のようになります。

package main

import (
	"html/template"
	"os"
)

type Args struct {
	Arg1 int
	Arg2 int
}

func main() {
	args := Args{Arg1: 1, Arg2: 2}

	comparison := "{{ lt .Arg1 .Arg2 }}"

	tmpl, err := template.New("test").Parse(comparison)
	if err != nil {
		panic(err)
	}

	err = tmpl.Execute(os.Stdout, args)
	if err != nil {
		panic(err)
	}
}

上記のコードを実行すると以下のように出力されます。

true

Actionsの種類

Actionsには以下の種類があります。それぞれ使い方を確認していきます。

comment

/* */で囲むことでコメントを記述することができます。

tmpl, _ := template.New("test").Parse("{{/* comment */}}") // comment
_ = tmpl.Execute(os.Stdout, map[string]string{"Arg1": "1", "Arg2": "2"})

// 出力 ※なにも表示されない
// 

pipeline

前回の記事で解説した{{ .Arg }}のような記述をpipelineと呼びます。 シンプルに与えた値が評価されます。

tmpl, _ := template.New("test").Parse("{{ .Arg }}")
_ = tmpl.Execute(os.Stdout, map[string]string{"Arg": "Hello, World"})

// 出力
// Hello, World

if pipeline T1 end

ifはpipelineの評価がtrueの場合に内容(if endで囲まれた記述)を出力します。

tmpl, _ := template.New("test").Parse("{{if .Bool}}T1{{end}}")
_ = tmpl.Execute(os.Stdout, map[string]bool{"Bool": true})

// 出力
// T1

if pipeline T1 else T0 end

if elseはpipelineの評価がtrueの場合にifで囲まれた記述を出力しfalseの場合はelseの内容を出力します。

tmpl, _ := template.New("test").Parse("{{if .Bool}}T1{{else}}T0{{end}}")
_ = tmpl.Execute(os.Stdout, map[string]bool{"Bool": false})

// 出力
// T0

if pipeline T1 else if pipeline T0 end

if else ifはpipelineの評価がtrueの場合にifで囲まれた記述を出力しfalseの場合はelse ifのpipelineを評価し内容を出力します。

tmpl, _ := template.New("test").Parse("{{if .Bool1}}T1{{else if .Bool2}}T0{{end}}")
_ = tmpl.Execute(os.Stdout, map[string]bool{"Bool1": false, "Bool2": true})

// 出力
// T0

.Bool1falseであるため無視されBool2trueであるためT0が出力されます。

range pipeline T1 end

rangeはpipelineがarray,slice,map,channelの場合に内容を繰り返し出力します。

tmpl, _ := template.New("test").Parse("{{range .}}T1{{end}}")
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// T1T1T1

上記実装の場合は与えた内容と関係なく配列の長さだけT1が出力されます。 空要素を与えた場合はなにも出力されません。

tmpl, _ := template.New("test").Parse("{{range .}}T1{{end}}")
_ = tmpl.Execute(os.Stdout, []string{})

// 出力 ※なにも表示されない
//

与えた内容を出力するには以下のように記述します。

tmpl, _ := template.New("test").Parse("{{range $index, $element := .}}index: {{$index}} element: {{$element}}\n{{end}}")
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// index: 0 element: a
// index: 1 element: b
// index: 2 element: c

$indexおよび$elementを定義し{{$index}}および{{$element}}で参照することで配列の内容を出力することができます。

range pipeline T1 else T0 end

range elserangeと同じくpipelineがarray,slice,map,channelの場合に内容を繰り返し出力します。また、pipelineが空の場合はelseの内容を出力します。

tmpl, _ := template.New("test").Parse("{{range .}}T1{{else}}T0{{end}}")
_ = tmpl.Execute(os.Stdout, []string{})

// 出力
// T0

break

breakrangeループの際に早期でループを終了することができます。if endと組み合わせて使用することで条件によってループを終了することができます。

tmpl, _ := template.New("test").Parse("{{range $i, $e := .}}{{if eq $i 1}}{{break}}{{end}}{{$e}}{{end}}")
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// a

$i1と等しい場合にbreakが実行されループが終了します。そのため配列0番目のaは出力さされ1以降のbcは出力されません。

continue

continuerangeループの際に実行をスキップすることができます。if endと組み合わせて使用することで条件によってループをスキップすることができます。

tmpl, _ := template.New("test").Parse("{{range $i, $e := .}}{{if eq $i 1}}{{continue}}{{end}}{{$e}}{{end}}")
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// ac

$i1と等しい場合にcontinueが実行されます。そのため配列1番目のbの出力がスキップされacが出力されます。

with pipeline T1 end

withはpipelineの値がnilや空でない場合に内容を出力します。

tmpl, _ := template.New("test").Parse("{{with .}}T1{{end}}")
_ = tmpl.Execute(os.Stdout, []string{"a", "b", "c"})

// 出力
// T1

公式ドキュメントにはpipeline is emptyと記載がありますが, ""0などのゼロ値はemptyとみなされるようです。

with pipeline T1 else T0 end

with elsewithと同じくpipelineの値がnilや空でない場合に内容を出力します。また、pipelineが空の場合はelseの内容を出力します。

tmpl, _ := template.New("test").Parse("{{with .}}T1{{else}}T0{{end}}")
_ = tmpl.Execute(os.Stdout, []string{})

// 出力
// T0

与えた値が空配列であるためT0が出力されます。

おわりに

Go Template について記事を書いてみたことによりだいぶ仲良くなれた気がします。 そろそろ Go Template を駆使してコード自動生成なんかにも挑戦してみたいです。 (今だとChatGPTにコード生成させた方が早いですかね…)