gin-gonicとdatabase/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を追加する