はじめに
メルカリさんの「プログラミング言語Go完全入門」を完走し、その後はGoプログラミング実践入門 標準ライブラリでゼロからWebアプリを作るという書籍を読み終えたので、ここら辺でGoだけを使った簡単なWEBアプリを作りたいと思い、TODOアプリを作成してみました。
こんなやつ作りました
簡易ですが、ご覧の通りです。HOMEボタンと、NEWボタン(新しくタスクを登録する)を上部に置き、その下はタスクを一覧表示しています。Editボタンで編集画面に行き、doneボタンで押下したタスクが削除されるという感じです。データベースにはPostgreSQLを使用しました。(まだ触ったことのないDBを触ってみたかったので)
ディレクトリ構成
todo-app
├ templates
│ ├ Edit.tmpl
│ ├ Header.tmpl
│ ├ Index.tmpl
│ ├ Menu.tmpl
│ └ New.tmpl
└ main.go
テーブル作成
create table todo(
id serial PRIMARY KEY,
task varchar(50)
);
メインパッケージ
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画面のみ載せておきます)
{{ 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
するたびに、ファイアウォールが出てしまっていたので、こちらを参考にさせていただき解消しました。
②
*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を使うを参考に、SERIAL型をidカラムに指定して、テーブルを作り直したところ解決しました。
最後に
PostgreSQLの使い方でかなり時間を使ってしまった感はあります。笑
しかし、今回の作成でDBとの接続方法や、ステートメントの投げ方、テンプレート(恐らく実務では使わなさそうですが)側と、処理側の値の表現方法が理解できました。次は、もう少し凝った物を作ろうと思います(テストも書けるようになりたい)。
Standard Go Project Layoutも目を通しておきたいな。また何かアプトプットができましたら、執筆しようと思います。
最後までご拝読いただき、有難うございます。
※まだまだ未熟なので、お気づきの点ございましたらコメント頂けますと幸いです。