Go Todo練習 day1

Posted on
go try

gin-gonicdatabase/sqlの基本的な動作は試せたので、早速ど定番のTodoアプリを作ってみます。やっぱ試してみないと不明点は見つかりにくいですしね!

day1ということで簡単な内容で今回は作ります

  • main.goのみ
  • DBはpostgres使う
  • TODOアプリで登録、取得のみ対応

作成したコードはsourjp/go-practice/day1にあります。

1. TODOを定義

ここでのポイントは FinishedAt の、*time.Time です。たとえ omitempty を使っていても、 time.Time のゼロ値は対象外(表示される)となります。

JSON omitempty With time.Time Fieldが参考になります。

そのため、pointerにしてnil扱いにすることでemptyと判定させることができます。 ただDBの視点で考えたときに、登録時にそもそもデータがないのだからomitempty関係なく空だからそりゃでないじゃんという・・・

構造体をMarshalする時には知っといた方がいいポイントかと思いました。

type TODO struct {
	ID         int        `db:"id" json:"id"`
	Title      string     `db:"title" json:"title"`
	Message    string     `db:"message" json:"message"`
	CreatedAt  time.Time  `db:"created_at" json:"created_at"`
	FinishedAt *time.Time `db:"finished_at" json:"finished_at,omitempty"`
}

/*
$ make t-get
[
    {
        "id": 1,
        "title": "study",
        "message": "study",
        "created_at": "2020-08-19T22:59:42.568475Z",
        "finished_at": "0001-01-01T00:00:00Z" <--- time.Timeのゼロ値で入れてしまった
    },
    {
        "id": 2,
        "title": "study2",
        "message": "study2",
        "created_at": "2020-08-19T23:14:50.603215Z" <--- *time.Timeにしたことで、emptyなのでfinished_atがない
    }
]

todo=# select * from todo;
 id | title  | message |         created_at         |     finished_at     
----+--------+---------+----------------------------+---------------------
  1 | study  | study   | 2020-08-19 22:59:42.568475 | 0001-01-01 00:00:00
  2 | study2 | study2  | 2020-08-19 23:14:50.603215 | 
*/

次にDBへの登録と取得の関数です。

func Put(db *sql.DB, todo TODO) error {
	if todo.CreatedAt.IsZero() {
		todo.CreatedAt = time.Now()
	}

	r, err := db.Exec("INSERT INTO todo (title, message, created_at, finished_at) VALUES ($1, $2, $3, $4)", todo.Title, todo.Message, todo.CreatedAt, todo.FinishedAt)

	if err != nil {
		return err
	}
	fmt.Println(r)
	return nil
}

func GetAll(db *sql.DB) ([]TODO, error) {
	rows, err := db.Query("SELECT * FROM todo")
	if err != nil {
		return nil, err
	}

	var t TODO
	var todos []TODO
	for rows.Next() {
		if err := rows.Scan(&t.ID, &t.Title, &t.Message, &t.CreatedAt, &t.FinishedAt); err != nil {
			return nil, err
		}
		todos = append(todos, t)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return todos, nil
}

2. DBを定義

conf定義やos.Getenv()などは次回に持ち越しで直接登録。

var (
	driver = "postgres"
	params = "host=localhost dbname=todo user=root password=root sslmode=disable"
)

func NewDB() (*sql.DB, error) {
	db, err := sql.Open(driver, params)
	if err != nil {
		return nil, err
	}
	if err := db.Ping(); err != nil {
		return nil, err
	}
	return db, nil
}

3. Handlerを定義

エラー定義は次回に持ち越します。gin.H{}に入れるエラーの構造体定義が次のステップでしょうか。

JSON.MarshalIndent に値するものは c.IndentedJSON のようです。ただインデントは 人間 にとってみやすいだけなので、Webサービスのようにシステムの連携ならばパフォーマンスのために c.JSON を使った方がいいのでしょうね。

IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body. It also sets the Content-Type as “application/json”. WARNING: we recommend to use this only for development purposes since printing pretty JSON is more CPU and bandwidth consuming. Use Context.JSON() instead.

func HandleGetAll(c *gin.Context) {
	db, err := NewDB()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"status": "Can't connect to DB"})
		return
	}
	todos, err := GetAll(db)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"status": err})
		return
	}

	if len(todos) == 0 {
		c.JSON(http.StatusOK, gin.H{"status": "there is no tods"})
		return
	}
	// c.JSON(http.StatusOK, todos)
	c.IndentedJSON(http.StatusOK, todos)
}

func HandlePut(c *gin.Context) {
	var t TODO
	if err := c.ShouldBindJSON(&t); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"status": "bad request"})
		return
	}
	db, err := NewDB()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"status": err})
	}

	if err := Put(db, t); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"status": err})
		return
	}

	c.JSON(http.StatusOK, gin.H{"status": "your recored recived"})
}

4. Routerを定義

作成したHandlerを定義

func main() {
	r := gin.Default()
	v1 := r.Group("/api/v1")
	{
		v1.GET("/getall", HandleGetAll)
		v1.POST("/create", HandlePut)
	}
	log.Println(r.Run())
}

5. そのほか

DBを定義します

CREATE TABLE IF NOT EXISTS todo (
    id SERIAL NOT NULL PRIMARY KEY,
    title VARCHAR(20),
    message VARCHAR(255),
    created_at TIMESTAMP,
    finished_at TIMESTAMP
)

手打ちは大変なのでMakefileを作ります。

run-1:
	@go run main.go

db-init:
	@docker run --name go-pg -e POSTGRES_USER=root -e POSTGRES_PASSWORD=root -e POSTGRES_DB=todo -p 5432:5432 -d postgres:12.4
	@sleep 5
	@psql -h localhost -U root -W root -d todo -f todo.sql

db-clean:
	@docker kill go-pg
	@docker rm go-pg

db-createtbl:
	@psql -h localhost -U root -W root -d todo -f todo.sql

t-get:
	@curl localhost:8080/api/v1/getall

t-post:
	@curl -X POST localhost:8080/api/v1/create -d '{"title": "study2", "message": "study2"}'

6. テスト

問題なく動きました

$ make db-init
c1b14ee3d376b1720300644a6e12b1ed2cb00b9ea814b43ee3a01befa5b97ecb
psql: warning: extra command-line argument "root" ignored
Password: 
CREATE TABLE

$ make run-1
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /api/v1/getall            --> main.HandleGetAll (3 handlers)
[GIN-debug] POST   /api/v1/create            --> main.HandlePut (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
{0xc00023ad20 1}
[GIN] 2020/08/20 - 00:05:56 | 200 |   22.708692ms |             ::1 | POST     "/api/v1/create"
[GIN] 2020/08/20 - 00:06:03 | 200 |   13.796575ms |             ::1 | GET      "/api/v1/getall"

$ make t-post
{"status":"your recored recived"}

$ make t-get
[
    {
        "id": 1,
        "title": "study2",
        "message": "study2",
        "created_at": "2020-08-20T00:05:56.643661Z"
    }
]

まとめ

とりあえず簡単に作ってみました。次はこれらを組み込もうと思います。

  • TODOにUpdate, Deleteを追加
  • Errorの型を作る
  • Packageを分ける

その次あたりの課題としてここら辺を入れていきます。

  • Testを書く
  • Todoに名前を追加する
  • User機能(signup/login/logout)あたりを追加する
  • Middleware - Authを追加する