Goのテンプレートをちゃんと使ってみる

# はじめに

Goのテンプレートのpackageをちゃんと使ってみたいという気持ちになったので調べました。

# 参考

[`text/template`パッケージのドキュメント](https://pkg.go.dev/text/template?tab=doc)
テンプレートの文法についてはこっちに書いてあります。

[`html/template`パッケージのドキュメント](https://pkg.go.dev/html/template?tab=doc)
packageのインタフェースは`text/template`と同じですが、セキュリティ上の問題を引き起こさないためには出力がHTMLの場合にはこちらを使うべきと書いてあります。

[Effective Go - The Go Programming Language # A web server](https://golang.org/doc/effective_go.html#web_server)
formに文字列を入力すると、GoogleのAPIを使ってQRコードの画像を生成して表示するサーバのサンプルコードがあります。`html/template`も登場します。

[Writing Web Applications - The Go Programming Language]
(https://golang.org/doc/articles/wiki/)
簡易的なWikiを作ってWebアプリの作り方を学べるチュートリアルです。重複を排除するようにリファクタしつつ進んでいくので教材としていい感じです。これも同じく、`html/template`も登場します。

# テンプレートの文法

文字列の中に`{{Action}}`の形式でデータや制御構造を埋め込める。

## Action
データの評価または制御構造を表す。

- コメント
  - `{{/* a comment */}}`の形式で出力されないコメントを書ける
- pipelineの値そのもの`{{pipeline}}`
- `if` `{{if pipeline}} T1 {{end}}`
  - pipelineの値がempty(`false`,`0`,`nil`のポインタかinterface,`len(x) == 0`の配列、スライス、map、文字列の場合)でなければT1、emptyなら何も出力されない
- `if-else` `{{if pipeline}} T1 {{else}} T0 {{end}}`
  - pipelineの値がemptyでなければT1、emptyならT0
- `if-else-if` `{{if pipeline}} T1 {{else if pipeline}} T0 {{end}}`
  - `else`に別の`if`を入れこむことができる
  - これと同じ結果になる`{{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}`
- `with` `{{with pipeline}} T1 {{end}}`
  - ifと同じだが、pipelineの値がemptyでない場合、dot(テンプレートに渡したデータ中の現在の参照位置を示す)がpipelineの値になる
- `with-else` `{{with pipeline}} T1 {{else}} T0 {{end}}`
  - pipelineの値がemptyでなければ`with`と同じ、emptyならdotは変わらない
- `range` `{{range pipeline}} T1 {{end}}`
  - pipelineの値が配列、スライス、map、channelのいずれかである必要がある
  - pipelineの値がempty(`len(x) == 0`)なら何も出力されない
  - emptyでない場合、要素の数だけループする
    - ループ中、dotは現在の要素を示す
- `range-else` `{{range pipeline}} T1 {{else}} T0 {{end}}`
  - `range`と同じだが、pipelineの値がemptyの場合T0が実行される

## Argument
単なる値を表す。ポインタでも自動的に値を見にいってくれる

- 表記：値
- Goの定数：untyped constantになる
- `nil`：untyped nil
- `.`：そのときのdotの値
- `$alphanum`の形式の変数名`$`も有効
- 構造体のフィールド名：フィールドの値
  - `.Field` の形式
  - `.Field1.Field2`ネストしていても参照できる
  - `$x.Field1.Field2`で変数のフィールドにもアクセスできる
- mapのキー名：キーの値
  - `.Key` の形式
  - `$x.key`変数に対しても同じことができる
- 引数を持たないメソッド名：メソッドの呼び出し結果
  - この形式で使えるメソッドは返り値を2つまでしか持てない
  - 2つの場合は、2つ目はerrorでないといけない
  - nilではないerrorが返った場合は、テンプレートの実行は止まりExecute()からそのerrorが返却される
- 引数を持たない関数名：関数の呼び出し結果
  - 返り値についての振る舞いはメソッドの場合と同じ

## Pipeline

1つ以上のcommand(単なる値あるいは関数かメソッドの呼び出し)を`|`で繋いだもの


# 関数

## 定義済みの関数

### `and`

引数のうち最初に見つかったemptyの引数を返す。emptyのものが1つもなかった場合は最後の引数を返す。

### `or`
引数のうち最初に見つかったemptyではない引数を返す。すべてemptyだった場合は最後の引数を返す。

### `not`
引数がemptyなら`true`、そうでなければ`false`を返す。

### `call`

最初の引数(関数じゃないといけない)に残りの引数を渡して実行した結果を返す。

https://play.golang.org/p/B7oAqxsBKaT

```go
package main

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

type Point struct {
	X float64
	Y float64
}

func Distance(a, b Point) float64 {
	return math.Sqrt((a.X-b.X)*(a.X-b.X) + (a.Y-b.Y)*(a.Y-b.Y))
}

const tmplStr = `
p1: {{printf "%#v" .p1}}
p2: {{printf "%#v" .p2}}
{{/* {{.distance .p1 .p2}} はダメ */}}
distance: {{call .distance .p1 .p2}}
`

var tmpl = template.Must(template.New("call").Parse(tmplStr))

func main() {
	data := map[string]interface{}{
		"p1":       Point{X: 3.0, Y: 0.0},
		"p2":       Point{X: 0.0, Y: 4.0},
		"distance": Distance,
	}
	if err := tmpl.Execute(os.Stdout, data); err != nil {
		panic(err)
	}
}

```
```

p1: main.Point{X:3, Y:0}
p2: main.Point{X:0, Y:4}

distance: 5
```

### `print`, `printf`, `println`

それぞれ`fmt.Sprint`, `fmt.Sprintf`, `fmt.Sprintln`の働きをする。

### `html`, `js`, `urlquery`

引数をそれぞれHTML,JavaScript,クエリストリングとしてエスケープする。

https://play.golang.org/p/JU9sPOW42NT

```go
package main

import (
	"os"
	"text/template"
)

const tmplStr = `
and: {{and 0 0}} {{and 1 0}} {{and 0 1}} {{and 1 1}}
or: {{or 0 0}} {{or 1 0}} {{or 0 1}} {{or 1 1}}

not: {{not "ok"}} {{0 | not}}

html: {{html "<a href=\"https://golang.org/\">Go</a>"}}
js: {{js "<script>alert('XSS');</script>"}}
urlquery: {{urlquery "http://example.com/path?key=val"}}
slice: {{slice "golang" 1 4}}

eq: {{eq "🍣" "🍣"}}	{{eq "🍣" "🍺"}}	{{eq "🍺" "🍣"}}
ne: {{ne "🍣" "🍣"}}	{{ne "🍣" "🍺"}}	{{ne "🍺" "🍣"}}
lt: {{lt "🍣" "🍣"}}	{{lt "🍣" "🍺"}}	{{lt "🍺" "🍣"}}
le: {{le "🍣" "🍣"}}	{{le "🍣" "🍺"}}	{{le "🍺" "🍣"}}
gt: {{gt "🍣" "🍣"}}	{{gt "🍣" "🍺"}}	{{gt "🍺" "🍣"}}
ge: {{ge "🍣" "🍣"}}	{{ge "🍣" "🍺"}}	{{ge "🍺" "🍣"}}
`

var tmpl = template.Must(template.New("functions").Parse(tmplStr))

func main() {
	if err := tmpl.Execute(os.Stdout, nil); err != nil {
		panic(err)
	}
}

```
```

and: 0 0 0 1
or: 0 1 1 1

not: false true

html: &lt;a href=&#34;https://golang.org/&#34;&gt;Go&lt;/a&gt;
js: \x3Cscript\x3Ealert(\'XSS\');\x3C/script\x3E
urlquery: http%3A%2F%2Fexample.com%2Fpath%3Fkey%3Dval
slice: ola

eq: true	false	false
ne: false	true	true
lt: false	true	false
le: true	true	false
gt: false	false	true
ge: true	false	true
```

## 自分で定義した関数を使う

`(*Template) Funcs()`で`template.FuncMap`(`map[string]interface{}`)を渡すと自分で定義した関数を使える。`Funcs()`は`Parse()`より前に呼ぶ必要がある。

https://play.golang.org/p/7jJOVbGBqPk

```go
package main

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

const tmplStr = `
multiply: {{multiply 2 3}}
average: {{average 2 3 5}}
round: {{round 3.14}}
average|round: {{average 2 3 5 | round}}
reverse: {{reverse "reverse"}}
sushi|beer: {{"" | sushi | beer | sushi | beer}}
`

var (
	funcMap = map[string]interface{}{
		"multiply": func(a, b int) int {
			return a * b
		},
		"average": func(nums ...int) float64 {
			sum := 0
			for _, n := range nums {
				sum += n
			}
			return float64(sum) / float64(len(nums))
		},
		"round": func(num float64) int {
			return int(math.Round(num))
		},
		"reverse": func(s string) string {
			r := []rune(s)
			for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
				r[i], r[j] = r[j], r[i]
			}
			return string(r)
		},
		"sushi": func(s string) string {
			return s + "🍣"
		},
		"beer": func(s string) string {
			return s + "🍺"
		},
	}
	tmpl = template.Must(template.New("funcmap").Funcs(funcMap).Parse(tmplStr))
)

func main() {
	if err := tmpl.Execute(os.Stdout, nil); err != nil {
		panic(err)
	}
}

```
```
multiply: 6
average: 3.3333333333333335
round: 3
average|round: 3
reverse: esrever
sushi|beer: 🍣🍺🍣🍺
```