Go言語でTODOアプリを作ってみた

# はじめに
メルカリさんの「プログラミング言語Go完全入門」を完走し、その後は[Goプログラミング実践入門　標準ライブラリでゼロからWebアプリを作る](https://www.amazon.co.jp/gp/product/B06XKPNVWV/ref=dbs_a_def_rwt_bibl_vppi_i0)という書籍を読み終えたので、ここら辺でGoだけを使った簡単なWEBアプリを作りたいと思い、TODOアプリを作成してみました。

# こんなやつ作りました
<img width="457" alt="top" src="https://user-images.githubusercontent.com/57692216/123016458-98e4f980-d405-11eb-9f86-7489ce0eedb2.png">
簡易ですが、ご覧の通りです。HOMEボタンと、NEWボタン（新しくタスクを登録する）を上部に置き、その下はタスクを一覧表示しています。Editボタンで編集画面に行き、doneボタンで押下したタスクが削除されるという感じです。

データベースにはPostgreSQLを使用しました。（まだ触ったことのないDBを触ってみたかったので）

#### ディレクトリ構成
todo-app
├ templates
│ ├ Edit.tmpl
│ ├ Header.tmpl
│ ├ Index.tmpl
│ ├ Menu.tmpl
│ └ New.tmpl
└ main.go

#### テーブル作成

```bash
create table todo(
　　　　id serial PRIMARY KEY,
　　　　task varchar(50)
);
```

#### メインパッケージ

```go:main.go
package main

import (
	"database/sql"
	"net/http"
	"text/template"

	_ "github.com/lib/pq"
)

type Todo struct {
	Id   int
	Task string
}

func main() {
	http.HandleFunc("/", Index)
	http.HandleFunc("/new", New)
	http.HandleFunc("/edit", Edit)
	http.HandleFunc("/create", Create)
	http.HandleFunc("/delete", Delete)
	http.HandleFunc("/update", Update)
	http.ListenAndServe("localhost:8080", nil)　・・・①
}

func dbConn() (db *sql.DB) {　・・・②
	psqlInfo := "host=localhost user=×× password=×× dbname=×× port=5432 sslmode=disable"
	db, err := sql.Open("postgres", psqlInfo)
	if err != nil {
		panic(err.Error())
	}
	return db
}

var tmpl = template.Must(template.ParseGlob("templates/*"))　・・・③

func Index(w http.ResponseWriter, r *http.Request) {
	db := dbConn()
	selDB, err := db.Query("SELECT * FROM todo;")
	if err != nil {
		panic(err.Error())
	}

	td := Todo{}
	res := []Todo{}
	for selDB.Next() {　・・・④
		var id int
		var task string
		err = selDB.Scan(&id, &task)
		if err != nil {
			panic(err.Error())
		}
		td.Id = id
		td.Task = task
		res = append(res, td)
	}
	tmpl.ExecuteTemplate(w, "Index", res)
}

func New(w http.ResponseWriter, r *http.Request) {
	tmpl.ExecuteTemplate(w, "New", nil)
}

func Edit(w http.ResponseWriter, r *http.Request) {
	db := dbConn()
	uId := r.URL.Query().Get("id")
	selDB, err := db.Query("SELECT * FROM todo　WHERE id=$1", uId)
	if err != nil {
		panic(err.Error())
	}

	td := Todo{}
	for selDB.Next() {
		var id int
		var task string
		err = selDB.Scan(&id, &task)
		if err != nil {
			panic(err.Error())
		}
		td.Id = id
		td.Task = task
	}
	tmpl.ExecuteTemplate(w, "Edit", td)
}

func Create(w http.ResponseWriter, r *http.Request) {
	db := dbConn()
	if r.Method == "POST" {
		task := r.FormValue("task")
		crtForm, err := db.Prepare("INSERT INTO todo(task) VALUES($1);")
		if err != nil {
			panic(err.Error())
		}
		crtForm.Exec(task)
	}
	http.Redirect(w, r, "/", 301)
}

func Delete(w http.ResponseWriter, r *http.Request) {
	db := dbConn()
	td := r.URL.Query().Get("id")
	delForm, err := db.Prepare("DELETE FROM todo WHERE id=$1;")
	if err != nil {
		panic(err.Error())
	}
	delForm.Exec(td)
	http.Redirect(w, r, "/", 301)
}

func Update(w http.ResponseWriter, r *http.Request) {
	db := dbConn()
	if r.Method == "POST" {
		task := r.FormValue("task")
		udtForm, err := db.Prepare("UPDATE todo SET task=$1 WHERE id=$2;")
		if err != nil {
			panic(err.Error())
		}
		udtForm.Exec(task)
	}
	http.Redirect(w, r, "/", 301)
}

```

#### テンプレート側(ここではTOP画面のみ載せておきます)

```html:Index.tmpl
{{ define "Index" }}
    {{ template "Header" }}
        {{ template "Menu" }}
        <table border="1">
            <thead>
                <tr>
                    <td>ID</td>
                    <td>TASK</td>
                    <td>Edit</td>
                    <td>Press [done] when you finish.</td>
                </tr>
            </thead>
            <tbody>
            {{ range . }}
                <tr>
                    <td>{{ .Id }}</td>
                    <td>{{ .Task }}</td>
                    <td><a href="/edit?id={{ .Id }}">Edit</a></td>
                    <td><a href="/delete?id={{ .Id }}">done</a><td>
                </tr>
            {{ end }}
            </tbody>
        </table>
{{ end }}
```

# 解説
①
あまり本編とは関係ないですが、私の環境（macOS BigSur v11.4, Go v1.15.1）では、`go run main.go`するたびに、ファイアウォールが出てしまっていたので、[こちら](https://ludwig125.hatenablog.com/entry/2020/06/10/064119)を参考にさせていただき解消しました。

②
*sql.DB型を戻り値に指定することによって、異なるそれぞれの関数から呼び出せるようにしました。

③
ParseGlob()は、パターンによってマッチしたファイルのリストを持つParseFilesを呼び出すことと同義のため、Must()の引数にとります。Must()は、変数の初期化に用いられ、*Template型を返すので、ここで定義しておきます。（あとで、ExecuteTemplate()でテンプレートをwriterに書き込むため）

④
SELECTしてきた値が格納されているselDBをNext()を使ってグルグル回してその値を取得して、Todo構造体型が入るスライスにどんどんappendしていきます。

# 工夫・苦戦した点
### DB接続にはGORMなどを使用せずの作成
対応するドライバーでもともとどのようにDB接続し、どのようにステートメントを投げるのか知りたかったので、今回はGORMなどは使いませんでした。（GORMもベースは同じ感じだと思いますが）

### PostgreSQLのテーブル作成時、idカラムにSERIAL型を設定していなかったため、AUTO_INCREMENTしてくれなかった
MySQLしか馴染みがないので、カラム作成したら勝手にINSERT時に値が自動連番で登録されると思っていたけど違いました。笑
[PostgreSQLでMySQLのAUTO_INCREMENTを使う](https://qiita.com/shonansurvivors/items/4522f15c5e9a30860bc5)を参考に、SERIAL型をidカラムに指定して、テーブルを作り直したところ解決しました。

# 最後に
PostgreSQLの使い方でかなり時間を使ってしまった感はあります。笑
しかし、今回の作成でDBとの接続方法や、ステートメントの投げ方、テンプレート（恐らく実務では使わなさそうですが）側と、処理側の値の表現方法が理解できました。次は、もう少し凝った物を作ろうと思います（テストも書けるようになりたい）。
[Standard Go Project Layout](https://github.com/golang-standards/project-layout/blob/master/README_ja.md)も目を通しておきたいな。また何かアプトプットができましたら、執筆しようと思います。
最後までご拝読いただき、有難うございます。
※まだまだ未熟なので、お気づきの点ございましたらコメント頂けますと幸いです。

# 参考
* [pq](https://github.com/lib/pq) v1.10.2
* [ローカルPCでGoでserverを立ち上げたときにファイアウォールを出なくさせる](https://ludwig125.hatenablog.com/entry/2020/06/10/064119)
* [PostgreSQLでMySQLのAUTO_INCREMENTを使う](https://qiita.com/shonansurvivors/items/4522f15c5e9a30860bc5)
* [[PostgreSQL] よく使うコマンドまとめ](https://dev.classmethod.jp/articles/postgresql-organize-command/)
* [nobonoboさんのZenn](https://zenn.dev/nobonobo)