Go Gin Docker AWSで完璧なTodoアプリを作る

こんにちは。
今回は最近流行りのGoの勉強ついでにQiitaを書いてみようと思います。

普段はReactでのフロント開発やRailsでAPI開発などをしています。
社内でもRailsからGoへの書き換えが始まっている中で、Goってどうやって書くの？インフラわからないんだけどデプロイしてみたい！って軽い気持ちでサンプルアプリを作ってみたいと思います。
意外とローカルで動くものを作って終わりな記事が多いので今回はデプロイまで行うことをゴールとして開発していきます！！

## 使用技術
- go
- gin
- go modules
- mysql
- docker
- gorm
- AWS

## 参考文献
[Go / Gin で超簡単なWebアプリを作る](https://qiita.com/hyo_07/items/59c093dda143325b1859)
[DockerでGoの開発環境を構築する](https://qiita.com/uji_/items/8c9eda89526abe0ba900)


## 早速やっていこーう（環境構築）
（ここではgoのインストールは省略しまーす。）
まずやることはディレクトリの作成！

```terminal
mkdir perfect_todo
cd perfect_todo
```

そして今回はgoのライブラリーを管理するためにgo modというものを使っていきます。これはどのライブラリーを入れたかを管理してくれるので、docker化した時や共同開発の時にとても便利らしい。。。

```terminal
go mod init perfect_todo
```
これで `go.mod` と `go.sum` というファイルが生成されたかと思います。

そしたら次にgoのフレームワークであるginをインストールしていきたいと思います。

```terminal
go get github.com/gin-gonic/gin
```

`go.mod` をみてみてください。これで `github.com/gin-gonic/gin` が追加されていたらインストールが完了しています！

それが成功したら

```terminal
touch main.go
```

でmain.goのファイルを作ります。このファイルが実行されて、ここでインポートされているファイルが読み込まれていくので、アプリケーションの肝となるファイルです。

```main.go
package main

import (
	"github.cogom/gin-gonic/gin"
)

func main() {
	router := gin.Default()
	router.LoadHTMLGlob("templates/*.html")

	router.GET("/", func(c *gin.Context){
		c.HTML(200, "index.html", gin.H{})
	})

	router.Run()
}
```

ここではginを使って、ルーティングを設定しています。/にアクセスが来たらindex.htmlを表示するといった具合に設定をしています。

そしてそのindex.htmlがなくては表示させるファイルがないのでindex.htmlを作成します！

templatesディレクトリーを作ってその下にindex.htmlを作成します

```templates/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample App</title>
</head>
<body>
    <h1>Hello World!!!</h1>
</body>
</html>
```

これでお待ちかねのHello Worldの準備が完了です！

```terminal
go run main.go
```

これを実行して `localhost:8080` にいくと。。。！？！？


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609824/bf1a2711-c0e7-670a-3da1-a93e23cec4ed.png)

ででーーーーーん！！
Hell World!

何回みてもこの感動は良いですよねえ。

## Docker化

ここではDockerの説明は省きますが、Dockerにしておくと環境の違いで苦しんだりすることがないのでDocker化しておきます。

```terminal
touch Dockerfile
```

で `Dockerfile` を作成します。

``` Dockerfile
FROM golang:latest
RUN mkdir /go/src/app
WORKDIR /go/src/app
ADD . /go/src/app
VOLUME /go/src/app
RUN go mod download
EXPOSE 8080
CMD "go" "run" "main.go"
```

途中の `go mod download` でさっき作成した `go.mod` から自動でライブラリーを読み込んでくれると！便利ですね。
あとは今までやってきたことをDockerにやってもらっているだけです！

では実行していきましょう！

```terminal
docker build -t perfect_todo .
docker run -it -p 8080:8080 perfect_todo
```

これでさっき実行した時と同じ挙動になったら成功です！！

## Todoの作成

ここまで実際にGinやdockerを使って、動作するところまでは確認できたのでTodoアプリの作成に入っていきます。

まずはTodoというモデルを作っていきましょう。
ここではTodoには「やること（text)」「現在の状況(status)」「期限(deadline)」があると考え、以下のように作りました。

```domain/todo.go
package domain

import (
	"github.com/jinzhu/gorm"
)

type Todo struct {
	gorm.Model
	Text   string
	Status Status
	Deadline   int
}

type Status int

const (
	Task Status = iota
	ThisWeek
	Doing
	Review
	Done
	Close
)
```

Statusはenumで定義しています。

次にDBとのデータのやりとりを定義します。読み取りや書き込みです。一般的なCRUDを定義したいと思います。

```infrastructure/db.go
package infrastructure

import (
	"fmt"
	"perfect_todo/domain"

	"github.com/jinzhu/gorm"
)

func DbInit() *gorm.DB {
	db, err := gorm.Open("mysql", "root:@tcp(localhost:3306)/perfect_todo?parseTime=true")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	db.AutoMigrate(&domain.Todo{})
	return db
}

func DbCreate(todo domain.Todo) {
	db, err := gorm.Open("mysql", "root:@tcp(localhost:3306)/perfect_todo?parseTime=true")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	db.Create(&todo)
}

func DbRead(id ...int) []domain.Todo {
	db, err := gorm.Open("mysql", "root:@tcp(localhost:3306)/perfect_todo?parseTime=true")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todos []domain.Todo
	db.Find(&todos)
	return todos
}

func DbUpdate(id int, text string, status domain.Status, deadline int) domain.Todo {
	db, err := gorm.Open("mysql", "root:@tcp(localhost:3306)/perfect_todo?parseTime=true")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todo domain.Todo
	db.First(&todo, id)
	todo.Text = text
	todo.Status = status
	todo.Deadline = deadline
	db.Save(&todo)
	return todo
}

func DbDelete(id int) {
	db, err := gorm.Open("mysql", "root:@tcp(localhost:3306)/perfect_todo?parseTime=true")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todo domain.Todo
	db.First(&todo, id)
	db.Delete(&todo)
}
```
一番上のDbinitでTodoのモデルをマイグレートし、他の関数を使ってデータの書き込みや読み取りなどを行います。

ここでdirectory構造が気になるかたがいるかもしれませんが、ここでは少しclean architectureを意識して作っているため、役割を分けてdirectoryを作成しています。興味のある方は調べてみてください。僕もまだ薄い知識なので直した方が良いところなど教えていただけると幸いです。

最後にtodoをPOSTするためのroutingを設定します。

```main.go
db := infrastructure.DbInit()
defer db.Close()

router.GET("/", func(c *gin.Context) {
	todos := infrastructure.DbRead()
	c.HTML(200, "index.html", gin.H{
		"todos": todos,
	})
})

router.POST("/new", func(c *gin.Context) {
	text := c.PostForm("text")
	rawStatus := c.PostForm("status")
	id, err := strconv.Atoi(rawStatus)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
	status := domain.Status(id)
	rawTime := c.PostForm("deadline")
	deadline, err := strconv.Atoi(rawTime)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
	}
	todo := domain.Todo{Text: text, Status: status, Deadline: deadline}

	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	infrastructure.DbCreate(todo)
	c.Redirect(302, "/")
})
```

ここは参考にした記事を見よう見まねで作っています。
ところどころ受け取った情報のキャストを行ったり、それに失敗した場合にはエラーを返すように設定しています。

そしてhtmlの方も変更を行います

```index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Sample App</title>
</head>
<body>
    <h2>追加</h2>
    <form method="post" action="/new">
        <p>内容<input type="text" name="text" size="30" placeholder="入力してください" ></p>
        <p>状態
            <select name="status">
                <option value="0">Task</option>
                <option value="1">ThisWeek</option>
                <option value="2">Doing</option>
                <option value="3">Review</option>
                <option value="4">Done</option>
                <option value="5">Close</option>
            </select>
        </p>
        <p>期限
            <input type="text" name="deadline">
        </p>
        <p><input type="submit" value="Send"></p>
    </form>
    <ul>
        {{ range .todos }}
            <li>内容：{{ .Text }}、状態：{{ .Status }}、期限：{{ .Deadline }}</li>
        {{end}}
    </ul>
</body>
</html>
```

これでTodoを登録して表示する最低限の機能は整ったはずです！

## docker-composeの導入

次にDBの方のDockerfileを作成します。

```mysql/Dockerfile
FROM mysql:5.7

RUN touch /var/log/mysql/mysqld.log
```

そしてアプリのコンテナとDBコンテナを繋げるためにdocker-composeを使っていきます。

```docker-compose.yml
version: '3'
services:
  api:
    build:
      context: .
      dockerfile: ./Dockerfile
    ports:
      - "8080:8080"
    volumes:
      - .:/go/src/app
    links:
      - db
    depends_on: 
      - db
  db:
    build: 
      context: .
      dockerfile: ./mysql/Dockerfile
    container_name: todo_db
    environment:
      MYSQL_USER: root
      MYSQL_ROOT_PASSWORD: hoge
      MYSQL_DATABASE: go_todo
      hostname: todo_db
      command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci
    ports: 
      - "3306:3306"
    volumes:
      - todo_db_data:/var/log/mysql
      
volumes:
  todo_db_data:
    driver: local
```

一番下のvolumesはDBのデータを永続化を行っています。これを行わないとコンテナを削除した際にデータが消えてしまいます。
他にもDBの文字設定もcommandで行っています。

そしてここで環境変数を使えるように.envを導入していきます。

```bash
go get github.com/joho/godotenv
```

新しく.envファイルを作成して、
```.env
MYSQL_USER=root
MYSQL_PASSWORD=hoge
MYSQL_DATABASE=go_todo
MYSQL_HOST=todo_db
```

そしてここで環境変数をgithubにpushしないように.gitignoreにも.envを追加しておきましょう。
.gitignoreを作ってなかった方はここで作ります。

```.gitignore
.env
```


環境変数を使うことにともなって、DB接続のソースコードも変えていきます。

```main.go
...

func Env_load() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}

func main() {
	Env_load()
	router := gin.Default()
	router.Use(middleware.RequestLogger())
	router.LoadHTMLGlob("templates/*.html")
...
```

```db.go
package infrastructure

import (
	"database/sql"
	"fmt"
	"go_todo/domain"
	"os"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func connectDB() *sql.DB {
	user := os.Getenv("MYSQL_USER")
	password := os.Getenv("MYSQL_PASSWORD")
	database := os.Getenv("MYSQL_DATABASE")
	host := os.Getenv("MYSQL_HOST")
	sqlDB, err := sql.Open("mysql", user+":"+password+"@tcp("+host+")/"+database+"?parseTime=true&loc=Local")
	if err != nil {
		fmt.Errorf("could not open database")
	}
	return sqlDB
}

func DBInit() *gorm.DB {
	sqlDB := connectDB()
	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
	if err != nil {
		fmt.Errorf("could not open database")
	}
	db.AutoMigrate(&domain.Todo{})
	return db
}

func DBCreate(todo domain.Todo) {
	sqlDB := connectDB()
	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
	if err != nil {
		fmt.Errorf("could not open database")
	}
	db.Create(&todo)
}

func DBRead(id ...int) []domain.Todo {
	sqlDB := connectDB()
	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todos []domain.Todo
	db.Find(&todos)
	return todos
}

func DBUpdate(id int, text string, status domain.Status, estimate int, time int) domain.Todo {
	sqlDB := connectDB()
	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todo domain.Todo
	db.First(&todo, id)
	todo.Text = text
	todo.Status = status
	todo.Estimate = estimate
	todo.Time = time
	db.Save(&todo)
	return todo
}

func DBDelete(id int) {
	sqlDB := connectDB()
	db, err := gorm.Open(mysql.New(mysql.Config{
		Conn: sqlDB,
	}), &gorm.Config{})
	if err != nil {
		fmt.Errorf("could not open database")
	}
	var todo domain.Todo
	db.First(&todo, id)
	db.Delete(&todo)
}

```
そして実行すると！！念願のdocker環境でtodoが取得、作成できる形に！！


### おまけ
途中でうまく動かなくなり、送られてきたリクエストのパラメーターをみたくなったので、middlewareを追加し、ログを吐くようにしました。

```infrastructure/middleware/request_logger.go
package middleware

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"

	"github.com/gin-gonic/gin"
)

func RequestLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		buf, _ := ioutil.ReadAll(c.Request.Body)
		rdr1 := ioutil.NopCloser(bytes.NewBuffer(buf))
		rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf))

		fmt.Println(readBody(rdr1))

		c.Request.Body = rdr2
		c.Next()
	}
}

func readBody(reader io.Reader) string {
	buf := new(bytes.Buffer)
	buf.ReadFrom(reader)

	s := buf.String()
	return s
}

```

```main.go
router := gin.Default()
router.Use(middleware.RequestLogger())
router.LoadHTMLGlob("templates/*.html")
```

middlewareを追加したのと、それをmain.goで読み込みます。
これで以下の画像のようにリクエストのボディーが出力されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609824/186e88ae-5828-085b-b443-9b73a9735466.png)

出力をもっと綺麗にしたり、見やすくしたりはあとで行っていこうかと思います。

また、一旦休憩。。。