Docker × Go × MySQL 環境構築してみた

## はじめに
最近、色んな企業のサービスを見ていて、Go言語が使われている事が多くどんなものかと勉強しています。
まずは環境構築してみたので下記へ手順をまとめておきます。

## 目次
・[DockerでGoを起動する](#dockerでgoを起動する)
・[GoでWebページを作成して、「Hello, World！」を出力する](#goでwebページを作成してhello-worldを出力する)
・[DockerでMySQLを起動する](#dockerでmysqlを起動する)
・[GoとMySQLを接続する](#goとmysqlを接続する)
・[GoでCRUD処理を実装](#goでcrud処理を実装)
・[Dockerでphpmyadminを起動する](#dockerでphpmyadminを起動する)
・[参考文献](#参考文献)

## DockerでGoを起動する
まずはDockerでGoを起動させます。
1\. 作業ディレクトリ直下へ以下ファイルを作成します。
　・Dockerfile
　・docker-compose.yml
　・main.go


```dockerfile:Dockerfile
FROM golang:1.17.1-alpine3.13 as builder

WORKDIR /go/src
```
```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!")
}
```

2\. ターミナルを開いて、``` docker-compose up ``` を実行します。
Hello, World!と出ればGoの起動は成功です！
![スクリーンショット 2023-05-24 22.24.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/5585219c-13e5-a2e9-5955-fff9105ec332.png)
・Dockerfile
Goのコンテナを作成します。
 ``` WORKDIR /go/src ```の指定により以降全ての動作は``` /go/src ```以下で行われます。

・docker-compose.yml
Dockerfileで作成したコンテナの起動設定を記載します。
Dockerfileに記載されたコンテナ起動後、``` go run main.go ```のコマンドを叩いてmain.goを起動します。

・main.go
Goの処理を書くところです。

## GoでWebページを作成して、「Hello, World！」を出力する
次にGoでWebページを作っていきます。フレームワークはGinを使います。
1\. 以下の通り、各ファイルの作成・更新を行います。
　・DockerfileへGinインストールを追記
　・docker-compose.ymlへportsの記述を追記
　・main.goの内容を変更
　・templates/index.htmlを作成


```dockerfile:Dockerfile
FROM golang:1.17.1-alpine3.13 as builder

WORKDIR /go/src

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

2\. 次に``` docker-compose build ```したい所ですが、main.goでBrokenImportエラーが出ています。
![スクリーンショット 2023-05-24 22.52.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/8ee137dd-4297-2276-c9de-93e406a4f906.png)
というかそもそもgo.modがなかったので作成していきます。
<a href="https://osamu-tech-blog.com/go-go-modules/#:~:text=%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A7%E5%8B%95%E4%BD%9C-,go.mod%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90,%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82&text=%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E5%90%8Dgithub.com%2Fgithub,%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8C%E4%BD%9C%E6%88%90%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82" 
id="reference-276fa6a0c0fbfcd02ae9">go.modの作成、go getによるモジュールとそのバージョン追記</a>
・``` go mod init github.com/「importpath」 ```
あまり意識してなかったですが、``` go mod init github.com/```の後は「importpath」の使用が推奨のようです。（詳しくは<a href="https://teratail.com/questions/217859" 
id="reference-276fa6a0c0fbfcd02ae9">こちら</a>）
・``` go get```
使用するモジュールとそのバージョンをgo.modファイルに追記するコマンド

<a href="https://qiita.com/haruyama480/items/10c5135ef96fed61128a" 
id="reference-276fa6a0c0fbfcd02ae9">BrokenImportエラーの原因、go.workの作成</a>
今回は``` go work init ```コマンドでgo.workを作成後、workspaceを示すuseを手入力しました。自分も理解がまだ曖昧なので別途記事を投稿しようと思います。（詳しくは<a href="https://goodbyegangster.hatenablog.com/entry/2022/10/11/081836" 
id="reference-276fa6a0c0fbfcd02ae9">こちら</a>）

3\. ``` docker-compose build ```、 ``` docker-compose up -d ```の順に行いコンテナを立ち上げます。
``` up ```実行後に``` docker ps ```してみましたが、コンテナがうまく起動していないようです。
![スクリーンショット 2023-05-25 0.24.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/6613136c-89b1-6f98-5505-849aa69a0c52.png)
何が原因？？？
ymlのvolumesとDockerfileのWORKDIRのパスが異なるのが原因みたいです。
![スクリーンショット 2023-05-25 1.22.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/f609b699-d09e-3b16-8ced-00bec133dcce.png)

再度試した所、問題なくコンテナが立ち上がりました！！！
![スクリーンショット 2023-05-25 1.29.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/9888de26-a056-d9e4-2904-de619badab0e.png)
[http://localhost:8080](http://localhost:8080)へアクセスしてみると...
ご覧の通り、Webページが表示されました👏
![スクリーンショット 2023-05-25 1.37.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/b93b0ba9-e6f1-d09d-db6c-cafff893633f.png)

## DockerでMySQLを起動する
GoでのWebページ作成ができました。次はサービスづくりにどうしても必要なDBへの接続、MySQLのコンテナを立ち上げていきます。

1\. docker-compose.ymlへ以下を追記します。
　・dbコンテナの記述
　・volumesの記述
```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:
      - .:/go/src
    ports:
      - 8080:8080

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

2\. ``` docker-compose up -d ```を叩くと、MySQLのコンテナが立ち上がリます。
![スクリーンショット 2023-05-25 22.32.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/112aa219-1fad-69ad-0c59-c88552151697.png)
![スクリーンショット 2023-05-25 22.34.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/8f1b13e0-31b2-8772-19d5-3aa2bad72d74.png)

## GoとMySQLを接続する
次はGoとMySQLを繋いでいきましょう。
接続にはsqlドライバーとGORMというフレームワークを使います。

1\. 以下を行います。
　・DockerfileにsqlドライバーとGORMのインストールを追記
　・docker-compose.ymlへ依存関係の記述を追記
　・main.goへDB接続の処理を追記
```dockerfile:Dockerfile
FROM golang:1.17.1-alpine3.13 as builder

WORKDIR /go/src

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:
      - .:/go/src
    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
}
```

2\. ``` docker compose up ```を行い、コンソールに「DB接続成功」と出たら成功です。
残念ながらエラーのようです(o^^o)
![スクリーンショット 2023-05-25 23.10.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/1b1c174e-29fb-64e8-0dac-d83b61648b9b.png)
go.modへバイナリ(ライブラリ)のインストールが必要とのこと。
なぜかDockerfileのRUNコマンドが実行されてない模様...。今回は以下を手動で実行します。
``` go get github.com/go-sql-driver/mysql ```
``` go get github.com/jinzhu/gorm ```

再度 ``` docker compose up ```した所、成功しました🎉
![スクリーンショット 2023-05-25 23.16.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/548e6892-fdb0-56af-5881-0825e0b017ed.png)

## GoでCRUD処理を実装
いよいよラストです！GoでCRUD処理を実装していきます。

1\. main.goとindex.htmlを以下の通り変更します。
　main.go
　　・Userの定義を作成
　　・Userテーブルを作成
　　・post処理(Userデータの登録・削除)の実装
　index.html
　　・ユーザー登録・削除機能の実装
```go:main.go
package main

import (
	"fmt"
	"strconv"
	"time"

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

type User struct {
	gorm.Model
	Name       string
	Department string
	Email      string
	Phone      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")
		department := ctx.PostForm("department")
		email := ctx.PostForm("email")
		phone := ctx.PostForm("phone")
		fmt.Println("create user " + name + " with email " + email)
		db.Create(&User{Name: name, Department: department, Email: email, Phone: phone})
		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>My App</title>
  <style>
    label {
      width: 200px;
      float:left;
      margin: 0 50px 25px 0;
    }
    p {
      margin-bottom: 5px;
    }
    input {
      height: 20px;
      margin-bottom: 10px;
    }
    tr {
      width: 50%;
    }
    th,td{
      border:1px #333 solid;
    }
    table{
      margin-top: 5px;
      width: 100%;
      border-collapse: collapse;
    }
    #tdSample {
      margin-left: 5px;
    }
    button {
      width: 75px;
      height: 30px;
      color: #FFF;
      background-color: #3CB371;
      border: 0;
      border-radius: 3px;
      cursor: pointer;
      font-weight: bold;
    }

  </style>
</head>
<body>
  <h2>ユーザー一覧</h2>
  <form method="post" action="/new" style="margin: 10px 50px 10px 50px;">
    <div style="width: 100%;">
      <label>
        <p>氏名</p>
        <input type="text" name="name" size="30" placeholder="入力してください">
      </label>
      <label>
        <p>所属部署</p>
        <input type="text" name="department" size="30" placeholder="入力してください">
      </label>
      <label>
        <p>メールアドレス</p>
        <input type="text" name="email" size="30" placeholder="入力してください">
      </label>
      <label>
        <p>電話番号</p>
        <input type="text" name="phone" size="30" placeholder="入力してください">
      </label>
      <div style="padding-top: 43px;">
        <button type="submit">登録</button>
      </div>
    </div>
  </form>


    <table style="width: 90%;margin: 10px 50px 10px 50px;">
      <tr>
        <th style="width: 50px;"></th>
        <th>氏名</th>
        <th>所属部署</th>
        <th>メールアドレス</th>
        <th>電話番号</th>
      </tr>
      {{ range .users }}
        <tr>
          <td>
            <form method="post" action="/delete/{{.ID}}">
              <div align="center">
                <button type="submit">削除</button>
              </div>
            </form>
          </td>
          <td><div id="tdSample">{{ .Name }}</div></td>
          <td><div id="tdSample">{{ .Department }}</div></td>
          <td><div id="tdSample">{{ .Email }}</div></td>
          <td><div id="tdSample">{{ .Phone }}</div></td>
        </tr>
      {{ end }}
    </table>
</body>
</html>
```

2\. ``` docker compose up ```を行い、[http://localhost:8080](http://localhost:8080)へアクセスします。
ユーザー登録フォームが表示され、ユーザーを登録すると一覧へ登録情報が表示されます。
![スクリーンショット 2023-05-30 22.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/065584a4-5e47-d02c-f024-be99ca3c79a1.png)

## Dockerでphpmyadminを起動する
先程、ラストと言いましたがついでなのでdb管理ツールであるphpmyadminの立ち上げまで行って終わりとします。

1\. docker-compose.ymlへphpmyadminのコンテナ設定を追記します。
```yml:docker-compose.yml
  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    environment:
      - PMA_ARBITRARY=1
      - PMA_HOST=db # mysqlのサービス名を指定
      - PMA_USER=go_test # phpmyadminへログインするユーザ
      - PMA_PASSWORD=password # phpmyadminにログインするユーザのパスワード
    links:
      - mysql
    ports:
      - 4040:80
    volumes:
      - ./phpmyadmin/sessions:/sessions
```

2\. ``` docker compose up -d ```を行い、[http://localhost:4040](http://localhost:4040) へアクセスします。
ユーザー一覧画面で登録したユーザー情報がusersテーブルで見れるかと思います。
![スクリーンショット 2023-05-31 1.37.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3427082/7e6851f9-0f88-5417-3ebb-e562558c9c87.png)

## 参考文献
- 構築で参考にした記事
<a href="https://qiita.com/higakin/items/d33981801da14b65a882#%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B" 
id="reference-276fa6a0c0fbfcd02ae9">【初心者向け】3分でgolang + docker(docker-compose)環境を構築する！</a>

- 構築エラー解決で参考にした記事
<a href="https://osamu-tech-blog.com/go-go-modules/#:~:text=%E3%83%A2%E3%83%BC%E3%83%89%E3%81%A7%E5%8B%95%E4%BD%9C-,go.mod%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90,%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E4%BD%9C%E6%88%90%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82&text=%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB%E5%90%8Dgithub.com%2Fgithub,%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8C%E4%BD%9C%E6%88%90%E3%81%95%E3%82%8C%E3%81%BE%E3%81%99%E3%80%82" 
id="reference-276fa6a0c0fbfcd02ae9">go.modの作成、go getによるモジュールとそのバージョン追記</a>
<a href="https://qiita.com/haruyama480/items/10c5135ef96fed61128a" 
id="reference-276fa6a0c0fbfcd02ae9">BrokenImportエラーの原因、go.workの作成</a>
<a href="https://github.com/nao-United92/go-mysql" target="_blank"><img alt="Github" src="https://img.shields.io/badge/GitHub-%2312100E.svg?&style=for-the-badge&logo=Github&logoColor=white" /></a>