【Go】テンプレートエンジンを使ってコンテンツを表示してみた

# はじめに
Goの標準で用意されているテンプレートエンジンを使って，HTMLページを表示させてみた．
# 環境
- macOS BigSur
- Go 1.17.5 darwin/arm64

# テンプレートエンジンとは
本題に入る前にテンプレートエンジンについて説明する．
テンプレートエンジンとは「テンプレート」と呼ばれる雛形に「データ」を組み込んで最終的なHTMLを生成するもの．Web用のテンプレートエンジンは多くの本格的なフレームワークに含まれている．Goも例外ではなく，数種のテンプレートエンジンが組み込まれている．通常，ハンドラがテンプレートエンジンを呼び出してデータをテンプレートに組み込み，出来上がったHTMLをクライアントに返す．簡単なイメージ図はこんな感じ．

<img width="700" alt="template-engine.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/de4d829b-521f-dc8f-41cc-553f19e5c214.png">

テンプレートエンジンには標準規格がないが，大まかに２つの種類がある．
1. ロジックなしのテンプレートエンジン
1. ロジック埋め込みテンプレートエンジン

ロジックなしのテンプレートエンジンは全くロジックの処理は行わず，文字列の置換だけを行う．表現とロジックを明確に分離することで，ロジックはハンドラによってのみ処理される．一方ロジック埋め込みテンプレートエンジンは，プログラミング言語のコードをテンプレートに埋め込み，実行時にテンプレートエンジンによって処理を行う．

# Goのテンプレートエンジン
Go言語のテンプレートエンジンは，ロジックなし型とロジック埋め込み型のハイブリッドである．下図はWebアプリにおけるGo言語のテンプレートエンジンの動作を表している．

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/6c7160d2-3949-23a6-4e7e-f5b8f264446e.png)

マルチプレクサはリクエストを受け付け、URLに対応するハンドラへ転送する（他の言語ではルーティングと呼んだりする）．ハンドラがテンプレートエンジンを呼び出して，使用するテンプレートを動的なデータと一緒に渡す．テンプレートエンジンはHTMLを生成して，ResponseWriterに生成したHTMLを書き込み，HTTPレスポンスにそれを追加する．
Go言語には標準ライブラリとして，[text/template](https://pkg.go.dev/text/template) と [html/template](https://pkg.go.dev/html/template) が用意されている．

# それでは実践
## 1. Hello World
まずは一番シンプルな，"Hello World"をテンプレートエンジンを使って表示させるものから始める．以下のようなテンプレートファイル`template.html`を用意する．`body`内の`{{ . }}`に注目．このドット「.」はアクションと呼び，テンプレートエンジンがテンプレートを実行したときに，この部分にデータを埋め込む．

```html:template.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Hello Worldを表示させる</title>
  </head>
  <body>
    <h1>{{ . }}</h1>
  </body>
</html>

```

次にハンドラ関数からテンプレートエンジンを呼び出す処理を記述する．`server.go`内に`handler`というハンドラ関数が定義されており，これがテンプレートエンジンを呼び出している．まず，`ParseFiles`関数でテンプレートファイルを解析し，`Execute`メソッドを呼び出してデータ（今回は"Hello World!"）をテンプレートのアクションに埋め込んでいる．

```go:server.go
package main

import (
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template.html")
	t.Execute(w, "Hello World!")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/", handler)
	server.ListenAndServe()
}
```

最後に`server.go`を実行して，http://localhost:8080/ に接続すると...

![スクリーンショット 2022-02-11 21.55.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/103efbf9-a5b2-0360-1693-b57959451f05.png)

"Hello World!"が表示されていることが確認できる．

## 2. 条件アクション
次に重要なアクションの一つである条件アクションを実装してみる．条件アクションとは，テンプレートに渡されたデータを`if` `else`などを用いて条件分岐させるアクションである．以下のような`template.html`を用意する．

```html:template.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>条件アクション</title>
  </head>
  <body>
    {{ if .key1 }}
      <h1>False</h1>
    {{ else if .key2 }}
      <h1>True</h1>
    {{ else }}
      <h1>False</h1>
    {{ end }}
  </body>
</html>
```

ここで，マップが
```go
map[string]string{"key1":"val1", "key2":"val2"}
```
のように定義されているとき，テンプレート内では
```html
<p>{{ .key1 }}</p>
```
のように指定することで，対応するバリューを表示させることができる．
```html:実行結果
<p> val1 </p>
```
ハンドラ関数を次のように定義する．今回は`Excute`メソッドで`bool`をセットしたマップをテンプレートのアクションに埋め込んでいる．
```go:server.go
package main

import (
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template.html")
	t.Execute(w, map[string]bool{
		"key1": false,
		"key2": true,
	})
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/", handler)
	server.ListenAndServe()
}

```
最後に`server.go`を実行して，http://localhost:8080/ に接続すると...
![スクリーンショット 2022-02-11 21.51.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/8569f0cd-1ab5-905e-6013-7b2a2e922168.png)

## 3. イテレータアクション
イテレータアクションとは，配列やスライス，マップの要素ごとに反復処理を行うアクションである．次のように記述する．
```html
{{ range array }}
 ドットには要素が入る {{ . }}
{{ end }}
```
次のようなテンプレートファイルとハンドラ関数を用意する．
```html:template.html
<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>イテレータアクション</title>
  </head>
  <body>
    <ul>
    {{ range . }}
      <li>{{ . }}</li>
    {{ end }}
    </ul>
  </body>
</html>

```

```go:server.go
package main

import (
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template.html")
	daysOfWeek := []string{"月", "火", "水", "木", "金", "土", "日"}
	t.Execute(w, daysOfWeek)
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/", handler)
	server.ListenAndServe()
}
```
ここでは曜日名を格納した文字列のスライスをテンプレートエンジンに渡し，実行時に `{{ range . }}` にあるドッドに渡され，スライスの各要素が順に処理される．最後に`server.go`を実行して，http://localhost:8080/ に接続すると...
![スクリーンショット 2022-02-12 11.43.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/358b6157-fd46-e12e-0155-ffaf6f81c3f1.png)

## 4. 代入アクション
代入アクションは，そのアクションで囲まれたセクション内で指定の値をドットに代入できるものである．次のように記述する．
```html
{{ with hoge }}
  ドットに {{ . }} が設定される
{{ end }}

//実行結果
ドットに hoge が設定される
```
以下のようなテンプレートファイルとハンドラ関数を用意する．
```html:template.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>代入アクション</title>
  </head>
  <body>
    <div>ドットの値は{{ . }}です</div>
    <div>
    {{ with "go"}}
      ドットは今、{{ . }}に設定されています
    {{ end }}
    </div>
    <div>ドットの値は{{ . }}に戻りました</div>
  </body>
</html>
```

```go:server.go
package main

import (
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("template.html")
	t.Execute(w, "hello")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/", handler)
	server.ListenAndServe()
}
```
最後に`server.go`を実行して，http://localhost:8080/ に接続すると...
![スクリーンショット 2022-02-14 18.54.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/3ea6e52e-07e4-87f4-5ef2-a55babecd2da.png)

## 5. インクルードアクション
インクルードアクションとは，テンプレートの中に別のテンプレートを差し込むアクションのこと．これによってテンプレートを入れ子にすることができる．インクルードアクションは`{{ template "name" . }}`のように記述する．templateの引数は，1つ目はインクルードするテンプレート名，2つ目はテンプレートに渡すデータを指定する．次のようなテンプレートファイル`template1.html`と`template2.html`を用意する．`template1.html`は`template2.html`をインクルードする．

```html:template1.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=9">
    <title>インクルードアクション</title>
  </head>
  <body>
    <div> ここは、template1.html（インクルードの前）</div>
    <div>template1.html内でのドットの値 - [{{ . }}]</div>
    <hr/>
    {{ template "template2.html" . }}
    <hr/>
    <div> ここは、template1.html（インクルードの後）</div>
  </body>
</html>
```
```html:template2.html
<div style="background-color: blue;">
  ここはtemplate2.htmlです<br/>
  template2.html内でのドットの値 - [{{ . }}]
</div>
```
テンプレートを作成するときに名前を設定していないため，テンプレート名にファイル名が使用されていることがわかる．テンプレート名を定義するには次のように記述する．
```html
{{ define "main" }}
  <div style="background-color: blue;">
   ここはtemplate2.htmlです<br/>
   template2.html内でのドットの値 - [{{ . }}]
  </div>
{{ end }}
```
こうすることでテンプレート名は`main`と定義された．
そして次のようなハンドラ関数を用意して実行すると
```go:server.go
package main

import (
	"html/template"
	"net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
	t, _ := template.ParseFiles("t1.html", "t2.html")
	t.Execute(w, "Hello World!")
}

func main() {
	server := http.Server{
		Addr: "127.0.0.1:8080",
	}
	http.HandleFunc("/", handler)
	server.ListenAndServe()
}
```
![スクリーンショット 2022-02-14 20.06.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/421931/ec5440e8-3f25-cb27-c95b-21bebe77f982.png)


# 参考にした記事
- [Goプログラミング実践入門](https://www.marlin-arms.com/support/goweb/)
- [HTMLテンプレート](https://www.twihike.dev/docs/golang-web/templates)