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と違うなって思いました。まだ試せてないですけど、ghppecoというものを使うといい感じに管理できるみたいです。

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しか触ったことなかったのでかなり新鮮で楽しかったです!

この記事が気に入ったらサポートをしてみませんか?