【Go言語】テンプレートエンジンでtableをレンダリングしてechoで表示してみた

## はじめに
Goの標準で用意されているテンプレートエンジン(html/template)を使って、htmlのtableをレンダリングし、echoでwebサーバーを建てて表示してみました。

## 環境
* macOS　Sonoma 14.4.1
* Go 1.22.3 darwin-arm64

## 表示したいもの
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3787613/f942a972-c091-357e-590b-203da0c575aa.png)

このようなテーブルを表示するhtmlを出力することがゴールとなります。
```html:html
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8">
<title>Table Rendering</title>
<link rel="stylesheet" href="/public/css/style.css" type="text/css">
<body>
<table>
    <thead>
        <tr>
            <th></th>
            <th>Left</th>
            <th>Center</th>
            <th>Right</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <th>Top</th>
            <td>0</td>
            <td>1</td>
            <td>2</td>
        </tr>
        <tr>
            <th>Center</th>
            <td>3</td>
            <td>4</td>
            <td>5</td>
        </tr>
        <tr>
            <th>Bottom</th>
            <td>6</td>
            <td>7</td>
            <td>8</td>
        </tr>
    </tbody>
</table>
</body>
</html>
```

```css:/public/css/style.css
thead {
    background-color: #ffccff;  /* table headerの背景色指定 */
}

tbody {
    background-color: #ccffff;  /* table bodyの背景色指定 */
}

th,td {
    border: solid 1px;          /* 枠線指定 */
}
```

## tableへのデータを渡す方法
テンプレートエンジンにデータを格納する際、
```<tbody></tbody>```において、どこで繰り返し処理をするか？を考えたとき、下記２通りのパターンを考えました。
#### パターン１
テンプレートは```<tr></tr>```までに留めておき、その間に1行ずつ```<th></th>```を１列＋```<td></td>```を3列分のhtmlタグ３行分格納した2次元のスライスで渡します。すなわち、4列×3行のスライスになります。
```<tbody></tbody>```の中に渡したい構造体：```<tr></tr>```の間に```[][]template.HTML```

#### パターン２
テンプレートで```<th></th>```や```<td></td>```まで作って、その間に1次元スライスのstringデータを渡します。テンプレート側で```<th></th>```は1回、```<td></td>```は{{range}}により3回繰り返されます。
```<tbody></tbody>```の中に渡したい構造体：```<th></th>```の間にstring、```<td></td>```の間に```[]string```

## 実践
### webサーバー立ち上げ
まず初めに、下記のコマンドでmoduleの初期化を行います。
```
go mod init SSR
```

次にmain.goを作成し、下記の通りechoでwebサーバーを建てます。

```go:main.go
package main

import (
	"SSR/model"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	e := echo.New()
	e.Use(middleware.Logger())
	e.Use(middleware.Recover())
	e.Static("/public/css", "./public/css") //cssファイルのアドレス指定
	e.GET("/1", model.ShowTable1)    //パターン1
	e.GET("/2", model.ShowTable2)    //パターン2
	e.Logger.Fatal(e.Start(":8080")) //ポート8080でwebサーバー起動
}
```
### データを渡すための構造体
まず構造体宣言のファイルを作成します。
Body_valの中身がパターン1及びパターン2の構造体に対応していて、```<tr></tr>```の間に渡されます。

```go:/model/struct.go
package model

import "html/template"

/* パターン1 */
type Table1 struct {
	Column   []string
	Body_val []Body1
}

type Body1 struct {
	Array [][]template.HTML    //2次元スライス
}

/* パターン2 */
type Table2 struct {
	Column   []string
	Body_val []Body2
}

type Body2 struct {
	Indexdata string
	Rowdata   []string         //1次元スライス
}
```

## テンプレートファイルの作成
構造体を流し込むためのテンプレートを下記の通り準備しました。
Body_valの中の構造体を渡すときに、```{{range $s:=.Body_val}}```として```$s```に置き換えておくことで、入れ子になっている構造体の中を取り出しやすくなります。
コツとして、```{{range ***}}```は繰り返したいタグの一つ上の行の最後に置き、
```{{end}}```は繰り返したいタグの直後に置くと変な改行が入らず、見やすいhtmlが出力されます。

#### パターン1
```html:template1.html.tmpl
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8">
<title>Table Rendering</title>
<link rel="stylesheet" href="/public/css/style.css" type="text/css">
<body>
<table>
    <thead>
        <tr>{{range .Column}}
            <th>{{.}}</th>{{end}}
        </tr>
    </thead>
    <tbody>{{range $s:=.Body_val}}{{range $s.Array}}
        <tr>{{ range .}}
            {{.}}{{end}}
        </tr>{{end}}{{end}}
    </tbody>
</table>
</body>
</html>
```
#### パターン2
```html:template2.html.tmpl
<!DOCTYPE html>
<html lang="ja">
<meta charset="utf-8">
<title>Table Rendering</title>
<link rel="stylesheet" href="/public/css/style.css" type="text/css">
<body>
<table>
    <thead>
        <tr>{{range .Column}}
            <th>{{.}}</th>{{end}}
        </tr>
    </thead>
    <tbody>{{range $s:=.Body_val}}
        <tr>
            <th>{{$s.Indexdata}}</th>{{range $s.Rowdata}}
            <td>{{.}}</td>{{end}}
        </tr>{{end}}
    </tbody>
</table>
</body>
</html>
```

## tableへのレンダリング
用意した構造体にデータを格納していきます。パターン1とパターン2でBody_valへの格納の仕方を見比べて見てください。


```go:/model/showtable.go
package model

import (
	"bytes"
	"html/template"
	"io"
	"log"
	"net/http"
	"strconv"

	"github.com/labstack/echo/v4"
)

/* パターン1 */
func ShowTable1(c echo.Context) error {
	var buffer bytes.Buffer     //一旦バッファーにhtmlを出力する
	tpl := template.Must(template.ParseFiles("template1.html.tmpl"))
	data := Table1{}
	data.Column = []string{"", "Left", "Center", "Right"}    //headerのカラム
	body_temp := Body1{}
	row := []string{"Top", "Center", "Bottom"}
	for i := 0; i < 3; i++ {
		htmlstring := []template.HTML{}
		htmlstring = append(htmlstring, template.HTML(`<th>`+row[i]+`</th>`))
		for j := 0; j < 3; j++ {
			numstring := strconv.Itoa(i*3 + j)
			htmlstring = append(htmlstring, template.HTML(`<td>`+numstring+`</td>`))
		}
		body_temp.Array = append(body_temp.Array, htmlstring)    //htmlタグを格納した4列×3行の２次元スライスを渡す
	}
	data.Body_val = append(data.Body_val, body_temp)
	err := tpl.Execute(&buffer, data)
	if err != nil {
		log.Print(err)
	}
	return c.HTML(http.StatusOK, buffer.String())
}

/* パターン2 */
func ShowTable2(c echo.Context) error {
	var buffer bytes.Buffer     //一旦バッファーにhtmlを出力する
	tpl := template.Must(template.ParseFiles("template2.html.tmpl"))
	data := Table2{}
	data.Column = []string{"", "Left", "Center", "Right"}    //headerのカラム
	row := []string{"Top", "Center", "Bottom"}
	for i := 0; i < 3; i++ {
		rowdata := Body2{}
		rowdata.Indexdata = row[i]
		for j := 0; j < 3; j++ {
			numstring := strconv.Itoa(i*3 + j)
			rowdata.Rowdata = append(rowdata.Rowdata, numstring)
		}
		data.Body_val = append(data.Body_val, rowdata)    //行ごとのデータを格納した1次元スライスを渡す
	}
	err := tpl.Execute(&buffer, data)
	if err != nil {
		log.Print(err)
	}
	return c.HTML(http.StatusOK, buffer.String())
}
```

## 動作確認
```
go mod tidy
go run main.go
```
とコマンドを打ち込んでwebサーバーを起動し、動作確認します。
ブラウザで下記URLを打ち込んで、上記テーブルが表示されれば完成です。

#### パターン1
```
http://localhost:8080/1
```
#### パターン2
```
http://localhost:8080/2
```

結果再掲
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3787613/f942a972-c091-357e-590b-203da0c575aa.png)