6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-06-23

はじめに

メルカリさんの「プログラミング言語Go完全入門」を完走し、その後はGoプログラミング実践入門 標準ライブラリでゼロからWebアプリを作るという書籍を読み終えたので、ここら辺でGoだけを使った簡単なWEBアプリを作りたいと思い、TODOアプリを作成してみました。

こんなやつ作りました

top 簡易ですが、ご覧の通りです。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)
);

メインパッケージ

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画面のみ載せておきます)

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するたびに、ファイアウォールが出てしまっていたので、こちらを参考にさせていただき解消しました。


*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も目を通しておきたいな。また何かアプトプットができましたら、執筆しようと思います。
最後までご拝読いただき、有難うございます。
※まだまだ未熟なので、お気づきの点ございましたらコメント頂けますと幸いです。

参考

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?