Java屋がGOでTODOアプリ
タイトル通り普段はCoineyでJavaを書いているのですが、ISUCONに出ることになったのでGOに入門してみます。IntelliJ IDEAを使う前提で話を進めていきます。
IntelliJ IDEAでGO書くときの準備手順はこちらを見てください。
今回使用するソースコードはこちらからお借りしました。ありがとうございます。
ではさっさく。ソースコードはこちらに置いてます
開発環境
go 1.10.3
mac OS Sierra
IntelliJ IDEA 2018.2
プロジェクトの作成
プロジェクトルートは$GOPATH/src/github.com/b1a9id/todoにしました。GOの決まりで$GOPATH/src/[レポジトリのドメイン(github.com/アカウント名)]/[アプリケーション名]にしなければなりません。第1つまずきポイントでした。
depの導入
depというパッケージ管理ツールを使います。JavaでいうところのMavenやGradleですかね。
$ brew install dep
$ cd $GOPATH/src/github.com/b1a9id/todo
$ dep init
これでdepがプロジェクトに追加されました。dep initはヘルプには次のように書いてあります。Gopkg.tomlとGopkg.lockが作られます。
Initialize a new project with manifest and lock files
コントローラの作成
package controller
import (
"net/http"
"github.com/gin-gonic/gin"
)
func IndexGET (c *gin.Context) {
c.String(http.StatusOK, "Hello world!");
}
index.goのソースコードです。github.com/gin-gonic/gin知らないしと怒られます。ginはGOのフレームワークです。IntelliJの困ったときの最強ショートカット「Option + Enter」の出番ですね。
実行してみると「go get github.com/gin-gonic/gin」じゃないかしら?と教えてくれます。このコマンドを実行するとginが落ちてきます。
github.comディレクトリ配下にgibが入るのでJavaと違うなって思いました。まだ試せてないですけど、ghpとpecoというものを使うといい感じに管理できるみたいです。
mainファイルの作成
package main
import (
"github.com/b1a9id/todo/src/controller"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/", controller.IndexGET)
router.Run(":8080")
}
main.goのソースコードです。
実行
main.goを実行します。ブラウザでlocalhost:8080にアクセスすると、「Hello world!」と表示されます。
データベースの用意
データベースは、mysqlを使っていきます。次のコマンドを実行してgwaデータベースを作ってください。
$ mysql -u root
mysql> CREATE DATABASE gwa;
マイグレーションツールは、参考記事と同様にgooseを使っていきます。使い方はREADME.mdを見てもらえればわかるかと思います。
$ go get -u github.com/pressly/goose/cmd/goose
マイグレーションファイルを置くディレクトリを作ります。
$ mkdir db
疎通確認
$ goose mysql "user:password@/dbname?parseTime=true" status
2018/08/11 17:16:11 Applied At Migration
2018/08/11 17:16:11 =======================================
マイグレーションファイルの作成
$ goose create init sql
2018/08/11 17:43:30 Created new file: 00001_init.sql
init.sql
-- +goose Up
-- SQL in this section is executed when the migration is applied.
CREATE TABLE IF NOT EXISTS task (
id INT UNSIGNED NOT NULL,
created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
title VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
);
-- +goose Down
-- SQL in this section is executed when the migration is rolled back.
DROP TABLE task;
マイグレーション状況の確認
$ goose mysql "[ユーザ]:[パスワード]@[db名]?parseTime=true" status
2018/08/12 16:07:47 OK 00001_init.sql
2018/08/12 16:07:47 goose: no migrations to run. current version: 1
マイグレーションの実行
$ goose mysql "[ユーザ]:[パスワード]@[db名]?parseTime=true" up
2018/08/12 16:07:47 OK 00001_init.sql
2018/08/12 16:07:47 goose: no migrations to run. current version: 1
$ goose mysql "[ユーザ]:[パスワード]@[db名]?parseTime=true" status
2018/08/12 16:07:53 Applied At Migration
2018/08/12 16:07:53 =======================================
2018/08/12 16:07:53 Sun Aug 12 16:07:47 2018 -- 00001_init.sql
MODELの準備(ORM使用)
参考記事と同様にxoというORMを使っていきます。
$ go get github.com/xo/xo
$ mkdir src/model
$ mkdir -p db/xo/templates
$ cp $GOPATH/src/github.com/xo/xo/templates/* db/xo/templates/
$ xo mysql://[ユーザ]:[パスワード]]@[host]/[db名] -o src/model/ --template-path db/xo/templates/
xoコマンドを実行したことでtask.xo.goというファイルが生成されました。
次にTask一覧を取得するControllerを実装します。
package controller
import (
"github.com/gin-gonic/gin"
"github.com/b1a9id/todo/src/model"
"database/sql"
"time"
"fmt"
"net/http"
"log"
)
func TasksGET(c *gin.Context) {
dbDriver := "mysql"
dbUser := "ユーザ名"
dbName := "gwa"
dbOption := "?parseTime=true"
db, err := sql.Open(dbDriver, dbUser + "@/" + dbName + dbOption)
if err != nil {
log.Fatal(err)
}
result, err := db.Query("SELECT * FROM task ORDER BY id DESC")
if err != nil {
panic(err.Error())
}
tasks := []model.Task{}
for result.Next() {
task := model.Task{}
var id uint
var createdAt, updatedAt time.Time
var title string
err = result.Scan(&id, &createdAt, &updatedAt, &title)
if err != nil {
panic(err.Error())
}
task.ID = id
task.CreatedAt = createdAt
task.UpdatedAt = updatedAt
task.Title = title
tasks = append(tasks, task)
}
fmt.Println(tasks)
c.JSON(http.StatusOK, gin.H{"tasks": tasks})
}
次にmain.goの実装します。今実装したコントローラのエンドポイントをルーターに追加します。
package main
import (
"github.com/b1a9id/todo/src/controller"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
v1 := router.Group("/api/v1")
{
v1.GET("/tasks", controller.TasksGET)
}
router.GET("/", controller.IndexGET)
router.Run(":8080")
}
次に初期データを挿入します。
mysql> insert into task (id, created_at, updated_at, title) values (1, '2018-01-01 11:00:00', '2018-01-01 12:00:00', 'TEST')
アプリケーションを起動して、http://localhost:8080/api/v1/tasksにアクセスすると。以下のように表示されます。
マイグレーションファイルの追加
idを自動採番するように定義を追加します。
$ cd db/
$ goose create auto_increment sql
2018/08/11 20:00:30 Created new file: 00002_auto_increment.sql
db/00002_auto_increment.sql
-- +goose Up
-- SQL in this section is executed when the migration is applied.
ALTER TABLE task MODIFY id INT UNSIGNED AUTO_INCREMENT NOT NULL;
-- +goose Down
-- SQL in this section is executed when the migration is rolled back.
ALTER TABLE task MODIFY id INT UNSIGNED NOT NULL;
マイグレーション実行
$ goose mysql "[ユーザ]:[パスワード]@[db名]?parseTime=true" up
2018/08/12 23:42:14 OK 00002_auto_increment.sql
2018/08/12 23:42:14 goose: no migrations to run. current version: 2
$ goose mysql "[ユーザ]:[パスワード]@[db名]?parseTime=true" status
2018/08/12 23:42:20 Applied At Migration
2018/08/12 23:42:20 =======================================
2018/08/12 23:42:20 Sun Aug 12 23:41:55 2018 -- 00001_init.sql
2018/08/12 23:42:20 Sun Aug 12 23:42:14 2018 -- 00002_auto_increment.sql
xoの更新
$ rm -rf src/model/
$ xo mysql://[ユーザ]:[パスワード]]@[host]/[db名] -o src/model/ --template-path db/xo/templates/
タスクの新規登録
src/controller/task.goにタスク登録処理を実装
....
....
func TaskPOST(c *gin.Context) {
dbDriver := "mysql"
dbUser := "root"
dbName := "gwa"
dbOption := "?parseTime=true"
db, err := sql.Open(dbDriver, dbUser + "@/" + dbName + dbOption)
if err != nil {
panic(err.Error())
}
title := c.PostForm("title")
now := time.Now()
task := &model.Task{
Title: title,
CreatedAt: now,
UpdatedAt: now,
}
err2 := task.Save(db)
if err2 != nil {
panic(err2.Error())
}
fmt.Printf("post sent. title %s", title)
}
src/main/main.goにルーティング情報を追加
...
v1.POST("/tasks", controller.TaskPOST)
...
POSTMANなどを使ってPOST localhost:8080/api/v1/tasksにアクセス
2 | 2018-08-13 15:45:21.786190 | 2018-08-13 15:45:21.786190 | Coiney
こんな感じでinsertが行われます。
登録済みタスクの更新
src/controller/task.goにタスク更新処理を実装
...
...
func TaskPATCH(c *gin.Context) {
dbDriver := "mysql"
dbUser := "root"
dbName := "gwa"
dbOption := "?parseTime=true"
db, err := sql.Open(dbDriver, dbUser + "@/" + dbName + dbOption)
if err != nil {
panic(err.Error())
}
// strconv.Atoiは文字列を10進数のint型に変換する
id, _ := strconv.Atoi(c.Param("id"))
task, err := model.TaskByID(db, uint(id))
if err != nil {
panic(err.Error())
}
title := c.PostForm("title")
now := time.Now()
task.Title = title
task.UpdatedAt = now
err = task.Update(db)
if err != nil {
panic(err.Error())
}
fmt.Println(task)
c.JSON(http.StatusOK, gin.H{"task": task})
}
src/main/main.goにルーティング情報を追加
...
v1.PATCH("/tasks/:id", controller.TaskPATCH)
...
POSTMANなどを使ってPATCH localhost:8080/api/v1/tasks/2にアクセス
2 | 2018-08-13 15:45:21.786190 | 2018-08-14 16:09:36.615420 | hey
更新されました。
タスクの削除
src/controller/task.goにタスク更新削除を実装
...
...
func TaskDELETE(c *gin.Context) {
dbDriver := "mysql"
dbUser := "root"
dbName := "gwa"
dbOption := "?parseTime=true"
db, err := sql.Open(dbDriver, dbUser + "@/" + dbName + dbOption)
if err != nil {
panic(err.Error())
}
id, _ := strconv.Atoi(c.Param("id"))
task, err := model.TaskByID(db, uint(id))
if err != nil {
panic(err.Error())
}
err = task.Delete(db)
if err != nil {
panic(err.Error())
}
c.JSON(http.StatusOK, "deleted")
}
src/main/main.goにルーティング情報を追加
...
v1.DELETE("/tasks/:id", controller.TaskDELETE)
...
POSTMANなどを使ってDELETE localhost:8080/api/v1/tasks/2にアクセス
削除されて、deletedという文字列が返ってくることが確認できました。
最後に
今回は雑にCRUDのAPIを実装することが目的だったので、けっこうリファクタ必要なところあるかと思います。(DBのアクセス情報設定しているところあたり)
サーバーサイド言語は基本Javaしか触ったことなかったのでかなり新鮮で楽しかったです!
この記事が気に入ったらサポートをしてみませんか?