Go言語×Docker：効率的な開発環境を構築し、Docker ComposeでMySQLと接続してみた

## はじめに
Googleが開発したプログラミング言語「**Go**」を学ぶために、Dockerを活用してMySQLコンテナと接続する手順を記録としてまとめました。

:::note info
***Dockerを使って「Hello, World!」を出力する手順を完了し、次のステップとしてMySQLコンテナの立ち上げ方法を実践していきます。***
:::

今回の記事では、Go言語を用いたMySQLコンテナとの連携について解説します。***この内容がどなたかの技術学習の支えになれば幸いです。***

## 書こうと思ったきっかけ
当初は、Goアプリケーションを動かすためのDockerfile作成を目標にしていました。

***これまでにも他のコンテナでDockerfileを作成した経験があったため、比較的スムーズに作成を終えることができました。***

:::note warn
**その後、「Hello, World!」の表示にも成功したので、次のステップとして、SQLなどのデータベースコンテナと接続することに挑戦してみました。**
:::

技術検証の過程では、いくつかエラーが発生し、その確認に時間がかかりましたが、最終的にブラウザ上で正常に動作する形に持っていくことができました。

:::note info
***今回の検証内容やコードについて、この記事で共有したいと思います。***
:::

## Go言語とDockerの知識整理
Go言語やDockerの基礎知識については、前回の記事で詳しくまとめていますので、必要に応じてそちらをご参照ください。

https://qiita.com/free-honda/items/27031bdf1b20e1cfae1f

また、Go言語が端末にインストールされていない場合は、初回の記事で環境構築の手順（Windows、Linux、Macそれぞれ）を解説しています。***まだ環境構築が済んでいない方は、そちらを参考にしてみてください。***

https://qiita.com/free-honda/items/b3672e2ccfedbb14773a

## 本記事を書くにあたり遭遇したエラーたち

この記事を投稿するにあたり、ローカル環境で試行錯誤する中でいくつかのエラーに遭遇しました。***そこで、それらを備忘録としてまとめておきます。***

### エラーその1: Goのバージョン指定エラー

このエラーは、`go.mod` ファイル内の **Goバージョン指定が誤っている** ことが原因でした。

- **有効な形式**: `go 1.20`
- **無効な形式**: `go 1.23.4`（エラー例）

**エラー詳細**

```bash
 => [app 4/6] RUN [ ! -f go.mod ] && go mod init my-go-app || true                                           0.1s 
 => ERROR [app 5/6] RUN go mod tidy                                                                          0.1s 
------
 > [app 5/6] RUN go mod tidy:
0.099 go: errors parsing go.mod:
[+] Running 0/1od:3: invalid go version '1.23.4': must match format 1.23
 ⠼ Service app  Building                                                                                     1.3s 
failed to solve: process "/bin/sh -c go mod tidy" did not complete successfully: exit code: 1
➜  my-go-app git:(honda-branch) ✗ 
```

**解決方法**

`go.mod` ファイルを確認し、現在のバージョン指定に誤りがないかチェックします。***不正なバージョン指定があった場合は、正しい形式に修正してください。***

### 遭遇したエラー その2: MySQLの起動完了前に接続を試みる問題
Goアプリケーションが、MySQLの起動完了を待たずに接続を試みたために発生したエラーです。

ログを見ると、`go_app` がデータベースに接続を試みた時点で、MySQLはまだ初期化中（`Initializing database files`）であることが分かります。

```bash
mysql_db  | 2025-01-12T21:04:10.100046Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.
go_app exited with code 1
```

**解決方法**

このように、MySQLが完全に起動していない状態でGoアプリケーションが接続を試みると、接続に失敗し、go_app コンテナが終了してしまいます（詳細は後述します）。

## ディレクトリ構造
以下のようなディレクトリ構造を使用します。

```tex
my-go-app/
├── main.go                # Goアプリケーションのエントリポイント
├── Dockerfile             # Goアプリケーション用のDocker設定ファイル
├── docker-compose.yml     # Docker Compose設定ファイル
├── wait-for-it.sh         # MySQLの起動を待機するスクリプト
├── go.mod                 # Goモジュール設定ファイル
├── go.sum                 # Goモジュール依存関係ファイル
└── README.md              # プロジェクト説明用（任意）
```

## 今回使用したコード
今回、私が使用した各ファイルのコードを以下に示します。このコードを利用することで、前述のエラーを解決することができました。

:::note alert
**※ただし、これはあくまで一例です。**
***使用する際にはご自身の環境に合わせて調整し、自己責任でご利用ください。***
:::

### 1. main.go

GoアプリケーションにMySQL接続機能を追加し、上記の修正内容を反映した最終的なコードです。

```main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"

	_ "github.com/go-sql-driver/mysql"
)

// データベース接続設定
const (
	dbUser     = "root"
	dbPassword = "password"
	dbHost     = "db"
	dbPort     = "3306"
	dbName     = "testdb"
)

func main() {
	// データベース接続文字列を構築
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", dbUser, dbPassword, dbHost, dbPort, dbName)

	// データベースに接続
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatalf("Failed to connect to database: %v", err)
	}
	defer db.Close()

	// データベース接続を確認
	err = db.Ping()
	if err != nil {
		log.Fatalf("Failed to ping database: %v", err)
	}
	fmt.Println("Connected to the database successfully!")

	// ハンドラ関数をルートパスに登録
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, World! Database connection is working!")
	})

	// サーバーをポート8080で起動
	fmt.Println("Server is running on port 8080...")
	http.ListenAndServe(":8080", nil)
}
```

`db.Ping()` を使って接続をチェックし、問題があればログにエラーメッセージを出力して終了します。

- HTTPハンドラの設定  
  `/` パスでリクエストを受け取った際に、MySQL接続が成功しているメッセージを返します。

- Docker環境との連携  
  `dbHost` はDocker Composeで設定したサービス名（例: `db`）と一致させる必要があります。

### 2. Dockerfile

Goで書かれたアプリケーションをビルドし、MySQLの準備が完了するのを待機してから起動するためのDockerイメージを作成しています。

```Dockerfile
FROM golang:1.20

WORKDIR /app

COPY . .

# wait-for-it.sh をコンテナにコピー
COPY wait-for-it.sh /wait-for-it.sh
RUN chmod +x /wait-for-it.sh

RUN [ ! -f go.mod ] && go mod init my-go-app || true
RUN sed -i 's/^go .*/go 1.20/' go.mod || true
RUN go mod tidy
RUN go build -o main .

CMD ["/wait-for-it.sh", "db:3306", "--", "./main"]

EXPOSE 8080
```

MySQLが起動するまで待機するためのスクリプト `wait-for-it.sh` をコンテナにコピーし、実行可能にしています。

- **ビルドとエントリポイント**  
  - Goアプリケーションをビルドして実行可能ファイルを生成します。  
  - 起動時に `wait-for-it.sh` を使用してMySQLの起動完了を待機し、アプリケーションを起動します。

- **ポート8080の開放**  
  アプリケーションで使用するポート8080を公開します。

### 3. docker-compose.yml

このファイルでは、サービス構成の定義や環境変数の設定など、GoアプリケーションとMySQLデータベースの連携に必要な設定を記述しています。

```docker-compose.yml
version: "3.8"

services:
  app:
    build:
      context: .
    ports:
      - "8080:8080"
    container_name: go_app
    depends_on:
      - db
    environment:
      DB_USER: root
      DB_PASSWORD: password
      DB_HOST: db
      DB_PORT: 3306
      DB_NAME: testdb

  db:
    image: mysql:8.0
    container_name: mysql_db
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: testdb
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:
```

- **サービス構成の定義**
  - **app**: Goアプリケーションをビルドして実行するサービス。
  - **db**: MySQLデータベースをホストするサービス。

- **ポートの設定**
  - **app**: ホストの8080ポートをコンテナの8080ポートにマッピング。
  - **db**: ホストの3306ポートをコンテナの3306ポートにマッピング。

- **環境変数の設定**
  - **app**: データベース接続情報（ユーザー名、パスワード、ホスト名、ポート番号、データベース名）を指定。
  - **db**: MySQLのルートパスワードと初期データベース名を指定。

- **依存関係の設定**
  - `depends_on` を使用して、**app** が **db** に依存していることを定義。

- **ボリュームの永続化**
  - `db_data` ボリュームを使用して、MySQLデータを永続化。

### 4. wait-for-it スクリプト
`wait-for-it` スクリプトを使用して、GoアプリケーションがMySQLの準備完了を待機するように設定します。

**手順**

プロジェクトディレクトリに `wait-for-it.sh` をダウンロードし、実行可能にします。

```bash
curl -o wait-for-it.sh https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh
chmod +x wait-for-it.sh
```

### 5. 必要な依存関係を追加

MySQLドライバーをインストールするために、以下のコマンドをプロジェクトディレクトリで実行します。

```bash
go mod init my-go-app
go get -u github.com/go-sql-driver/mysql
```

### 6. 起動手順
Docker Composeを使用して、アプリケーションをビルドし起動します。

```bash
docker compose up --build
```

## 実際にブラウザでアクセスしてみた

ブラウザで確認 ブラウザで `http://localhost:8080` にアクセスし、「`Hello, World! Database connection is working!`」が表示されることを確認します。

**表示例**

![Screenshot 2025-01-13 at 6.49.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/a75c990c-e656-d2ad-11f3-0432453da135.png)

また、ターミナル上では以下のように、`go_app | Server is running on port 8080...` と出力され、正常に起動していることが確認できます。

**ターミナル表示例**

![Screenshot 2025-01-13 at 6.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3662571/d0370229-2831-05ef-9df7-2a6760acf7cf.png)

## まとめ

ここまで読んでいただきありがとうございました！
今回は、最近興味を持ったGo言語を学ぶ中で、MySQLコンテナとの連携に挑戦し、技術検証を行いました。

:::note warn
***エラー修正や調査に少し時間がかかりましたが、最終的に自己解決し、ブラウザ上でアプリケーションを動かすことができ、とても満足しています。***
:::

***次回の記事では、立ち上げたMySQLコンテナにログインし、データベースの中身を確認する方法について詳しくご紹介します。***