Go+MySQL+Dockerで簡単なCRUDを実装する

## はじめに
先日、「業務で使う予定ないとはいえGoぐらいある程度知っておいたほうがいいよな...」と思い、Goを使って簡単なCRUDを実装してみたのでそのやり方を備忘録としてまとめておきます。

基本的には以下のサイトの内容を組み合わせて少しアレンジしたものになっています。Goの勉強をする上でこれらのサイトには非常にお世話になったのでこちらもご参考ください。
[DockerでGoの開発環境を構築する](https://qiita.com/uji_/items/8c9eda89526abe0ba900)
[Go / Gin で超簡単なWebアプリを作る](https://qiita.com/hyo_07/items/59c093dda143325b1859)
[Go言語（Golang）入門～MySQL接続編～](https://rightcode.co.jp/blog/information-technology/golang-introduction-mysql-connection)
[docker-compose MySQL8.0 のDBコンテナを作成する](https://qiita.com/ucan-lab/items/b094dbfc12ac1cbee8cb)
[docker-compose upでMySQLが起動するまで待つ方法(2種類紹介)](https://qiita.com/sennninn6/items/256c2d306b3c6c8f86cd)

## GoをDockerで立ち上げる
まずはGoをDockerで立ち上げていきます。
作業ディレクトリ直下に
- DockerFile
- docker-compose.yml
- main.go
を作成します

```dockerfile:DockerFile
FROM golang:latest

RUN mkdir /app
WORKDIR /app
```

```yml:docker-compose.yml
version: '3'
services:
  go:
    build:
      context: .
      dockerfile: DockerFile
    command: /bin/sh -c "go run main.go"
    stdin_open: true
    tty: true
    volumes:
      - .:/app
```

```go:main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello, World!")
}
```

これで`docker-compose up`を行うとコンソール上にHello, World!と出てくると思います。
これが出ればまずGoの起動は成功です。

簡単に各ファイルの解説をします。
・DockerFile
Goのコンテナ(仮想環境)を作成します。
ここで`WORKDIR /app`を指定していることで以降の動作をすべて`/app`以下で行ってくれます。

・docker-compose.yml
DockerFileで作ったコンテナを立ち上げるときの設定などを書きます。
これにより、DockerFileにあるコンテナを立ち上げてその中で`go run main.go`のコマンドを叩いてmain.goを起動します

・main.go
Goに関する処理はここに書いていきます。今回はHello, World!を出力するだけで終了しています。

## GoでWebページを作成する
とりあえずGoの起動ができたので次はGoを使ってWebページを作っていきましょう。
今回はGinというフレームワークを使ってみます。
- DockerFileにインストールを追加
- docker-compose.ymlにportsの記述を追加
- main.goの内容を書き換え
- templates/index.htmlを作成
を行ってください。

```docker:DockerFile
FROM golang:latest

RUN mkdir /app
WORKDIR /app

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

```yml:docker-compose.yml
version: '3'
services:
  go:
    build:
      context: .
      dockerfile: DockerFile
    command: /bin/sh -c "go run main.go"
    stdin_open: true
    tty: true
    volumes:
      - .:/app
    ports:
      - 8080:8080
```

```go:main.go
package main

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

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

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

  router.Run()
}
```

```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>
```

これで

```
docker-compose build
docker-compose up -d
```
を行ってしばらく待ってから
[http://localhost:8080](http://localhost:8080)にアクセスするとHello World!と表示されると思います。

今回やったことを解説します。
今回はGinというフレームワークを追加しました。
GinはDockerFileでコンテナ作成後に`go get github.com/gin-gonic/gin`というコマンドでインストールされ、main.goで呼び出されます。
そしてmain.goの中でtemplatesの中身が読み取られ、

```
router.GET("/", func(ctx *gin.Context){
  ctx.HTML(200, "index.html", gin.H{})
})
```
によってroot("/")に対してtemplates/index.htmlが紐づけられることになります。
ちなみにrouter.GETの第一引数("/")を"/test"などに変えると、[http://localhost:8080](http://localhost:8080)ではなく、[http://localhost:8080/test](http://localhost:8080/test)でindex.htmlが表示されるようになります。

最後にdocker-compose.ymlにportを追加することでlocalhost:8080へのアクセスを可能にしています。

## DockerでMySQLを起動する
ここまででGoでのWebページ作成はできるようになりました。しかし、実際にはWebサービスを作るときにDBとの接続は避けて通れない内容になってきます。
そこで次はDockerを使ってMySQLを立ち上げていきます。

まずはdocker-compose.ymlに
・dbコンテナについての記述
・volumeの記述
を追加してください。

```yml:docker-compose.yml
db:
  image: mysql:8.0
  environment:
    MYSQL_ROOT_PASSWORD: root
    MYSQL_DATABASE: go_database
    MYSQL_USER: go_test
    MYSQL_PASSWORD: password
    TZ: 'Asia/Tokyo'
  command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
  volumes:
    - db-data:/var/lib/mysql
    - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
  ports:
    - 3306:3306

volumes:
  db-data:
    driver: local
```

また、dbディレクトリを作り、その中にmy.cnfファイルを作成してください。

```:my.cnf
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_bin

default-time-zone = SYSTEM
log_timestamps = SYSTEM

default-authentication-plugin = mysql_native_password

[mysql]
default-character-set = utf8mb4

[client]
default-character-set = utf8mb4
```
(この辺は参考ページそのままです。ログに関する部分だけなぜか上手くいかなかったので外しています)

ここまでやって`docker-compose up -d`をやるとMySQLのコンテナも立ち上がるはずです。
設定の記述しかないのでここの説明は省略します。

## GoとMySQLを接続する
MySQLが立ち上がったので早速Goにつないでみます。
今回は接続にsqlドライバーとGORMというフレームワークを使います。
- DockerFileにインストールの追加
- docker-compose.ymlに依存関係の記述を追加
- main.goにDB接続の処理を追加
を行ってください。

```docker:DockerFile
FROM golang:latest

RUN mkdir /app
WORKDIR /app

RUN go get github.com/gin-gonic/gin
RUN go get github.com/go-sql-driver/mysql
RUN go get github.com/jinzhu/gorm
```

```yml:docker-compose.yml
version: '3'
services:
  go:
    build:
      context: .
      dockerfile: DockerFile
    command: /bin/sh -c "go run main.go"
    stdin_open: true
    tty: true
    volumes:
      - .:/app
    ports:
      - 8080:8080
    depends_on:
      - "db"

  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: go_database
      MYSQL_USER: go_test
      MYSQL_PASSWORD: password
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - db-data:/var/lib/mysql
      - ./db/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - 3306:3306

volumes:
  db-data:
    driver: local
```

```go:main.go
package main

import (
  "fmt"
  "time"

  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  _ "github.com/go-sql-driver/mysql"
)

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

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

  router.Run()
}

func sqlConnect() (database *gorm.DB) {
  DBMS := "mysql"
  USER := "go_test"
  PASS := "password"
  PROTOCOL := "tcp(db:3306)"
  DBNAME := "go_database"

  CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
  
  count := 0
  db, err := gorm.Open(DBMS, CONNECT)
  if err != nil {
    for {
      if err == nil {
        fmt.Println("")
        break
      }
      fmt.Print(".")
      time.Sleep(time.Second)
      count++
      if count > 180 {
        fmt.Println("")
        fmt.Println("DB接続失敗")
        panic(err)
      }
      db, err = gorm.Open(DBMS, CONNECT)
    }
  }
  fmt.Println("DB接続成功")

  return db
}
```
これで`docker compose up`を行い、コンソールに「DB接続成功」と出たら成功です。

追加された内容はsqlConnectがメインなのでそこを解説します。

```go
func sqlConnect() (database *gorm.DB) {
  DBMS := "mysql"
  USER := "go_test"
  PASS := "password"
  PROTOCOL := "tcp(db:3306)"
  DBNAME := "go_database"

  CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
  
  count := 0
  db, err := gorm.Open(DBMS, CONNECT)
  if err != nil {
    for {
      if err == nil {
        fmt.Println("")
        break
      }
      fmt.Print(".")
      time.Sleep(time.Second)
      count++
      if count > 180 {
        fmt.Println("")
        fmt.Println("DB接続失敗")
        panic(err)
      }
      db, err = gorm.Open(DBMS, CONNECT)
    }
  }
  fmt.Println("DB接続成功")

  return db
}
```
前半部はDBに接続するための情報を定義しています。docker-compose.ymlで設定した内容を入力してください。
その後、`db, err := gorm.Open(DBMS, CONNECT)`でDBに接続します。しかし、MySQLの起動時間によってはこのコマンドが実行される時点でMySQLの準備が完了していない場合があります。
そこでこのコードでは２つの対策をしています。

１つめはdocker-compose.ymlでの依存関係の設定です。
ここでdepends_onを設定したことにより、dbコンテナが立ち上がってからgoコンテナが立ち上がるようになります。

２つめはリトライ処理です。
dbコンテナが起動してからもMySQLが立ち上がるまでに時間がかかるのでもしDBにつながらなかった場合に１秒待ってからリトライするようにしています。
これだと本当にエラーのときにリトライし続けてしまうので適当な回数でエラーを返すようにします。このコードでは３分つながらなかったらエラーになるようになっています。

## CRUDを実装する
ついにMySQLにもつながるようになったので最後にCRUDの処理を実装して実際の流れをみていきましょう。
あと変更するのはmain.goとindex.htmlのみです。
- Userの定義を作成
- マイグレーション
- post処理の実装
- ユーザー追加フォーム、ユーザー削除ボタンの実装
をやっていきます。

```go:main.go
package main

import (
  "fmt"
  "strconv"
  "time"

  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  _ "github.com/go-sql-driver/mysql"
)

type User struct {
  gorm.Model
  Name string
  Email string
}

func main() {
  db := sqlConnect()
  db.AutoMigrate(&User{})
  defer db.Close()

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

  router.GET("/", func(ctx *gin.Context){
    db := sqlConnect()
    var users []User
    db.Order("created_at asc").Find(&users)
    defer db.Close()

    ctx.HTML(200, "index.html", gin.H{
      "users": users,
    })
  })

  router.POST("/new", func(ctx *gin.Context) {
    db := sqlConnect()
    name := ctx.PostForm("name")
    email := ctx.PostForm("email")
    fmt.Println("create user " + name + " with email " + email)
    db.Create(&User{Name: name, Email: email})
    defer db.Close()

    ctx.Redirect(302, "/")
  })

  router.POST("/delete/:id", func(ctx *gin.Context) {
    db := sqlConnect()
    n := ctx.Param("id")
    id, err := strconv.Atoi(n)
    if err != nil {
      panic("id is not a number")
    }
    var user User
    db.First(&user, id)
    db.Delete(&user)
    defer db.Close()

    ctx.Redirect(302, "/")
  })

  router.Run()
}

func sqlConnect() (database *gorm.DB) {
  DBMS := "mysql"
  USER := "go_test"
  PASS := "password"
  PROTOCOL := "tcp(db:3306)"
  DBNAME := "go_database"

  CONNECT := USER + ":" + PASS + "@" + PROTOCOL + "/" + DBNAME + "?charset=utf8&parseTime=true&loc=Asia%2FTokyo"
  
  count := 0
  db, err := gorm.Open(DBMS, CONNECT)
  if err != nil {
    for {
      if err == nil {
        fmt.Println("")
        break
      }
      fmt.Print(".")
      time.Sleep(time.Second)
      count++
      if count > 180 {
        fmt.Println("")
        panic(err)
      }
      db, err = gorm.Open(DBMS, CONNECT)
    }
  }

  return db
}
```

```html:templates/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="name" size="30" placeholder="入力してください" ></p>
    <p>メールアドレス<input type="text" name="email" size="30" placeholder="入力してください" ></p>
    <p><input type="submit" value="Send"></p>
  </form>
  
  <h2>ユーザー一覧</h2>
  <table>
    <tr>
      <td>名前</td>
      <td>メールアドレス</td>
    </tr>
    {{ range .users }}
      <tr>
        <td>{{ .Name }}</td>
        <td>{{ .Email }}</td>
        <td>
          <form method="post" action="/delete/{{.ID}}">
            <button type="submit">削除</button>
          </form>
        </td>
      </tr>
    {{ end }}
  </ul>
  </body>
</html>
```

これで`docker-compose up -d`を行い、[http://localhost:8080](http://localhost:8080)にアクセスするとユーザー登録フォームが現れ、ユーザーを登録すると下に登録したユーザーの情報が表示されるようになります。
また、コンテナを削除して上げ直しても登録されたユーザーは削除されず、ユーザー一覧に表示が残るようになります。

それでは追加部分の解説をしていきます。
まず、main.goでUserという構造体を作成しています。`gorm.Model`でidなどモデルに必要な内容をUserに入れ、更にUser特有のname, emailを追加しています。
この構造は`db.AutoMigrate`によってDBに反映されます。

続いて各パスでのCRUD処理を実装していきます。

rootパスではユーザー一覧を取得します。
db.Find(&users)でDB内にあるユーザー一覧をUser構造として取得します。
間にOrderを挟むことで取得時に古いユーザーが上に来るようにしています。
最後に取得したユーザーをindex.htmlに渡しています。

/newパスではフォームの内容をもとにユーザーを作成しています。
ctx.PostFormでフォームによってsubmitされた内容を取得し、その内容をdb.Createで永続化しています。
処理が終わったらrootにリダイレクトします。

/deleteパスではidを指定してユーザーを削除しています。
こちらではURLにユーザーのidを指定しているのですが、同様にctxから取得します。
そしてその内容からdb.Firstでユーザーを取得し、db.Deleteで該当のユーザーを削除します。
ここで、idはstringで渡されているのでstrconv.Atoiでint型に変換していることに注意してください。

index.htmlでは一般的なhtmlの書き方でformとtableを作成しています。
ここで、`{{ range .users }}`という形でmain.goから渡されたusersを受け取っています。

## おわりに
今回はGoでのWebサービス開発の導入としてGo+MySQL+Dockerで簡単なCRUDを実装してみました。あくまで練習なのでバリデーションとか細かい制御などは考えていません。
今回行った内容は初歩ではありますが、この内容を広げて複雑化していくことで実際にWebサービスを作ることができると思います。
もしGoで何か作ってみたいと考えている方がいらっしゃったらぜひ参考にしてみてください！