【第13回】GoとMySQLをDockerで繋いでみよう

バックエンドとしてGo言語を使ってMySQLと繋いでみましょう。

Homeブログ一覧【第13回】GoとMySQLをDockerで繋いでみよう

Goとは

GoはGoogleが開発したプログラミング言語であり、静的型付け、C言語に則ったコンパイル言語、メモリ安全性、ガベージコレクション、並行性の特徴を持ちます。

主にWebアプリケーションのバックエンド開発に使われます。それは、型安全性や開発ツールの豊富さ、パフォーマンスの優位性、Dockerとの相性が良いことなどが理由です。

Goのインストール

Ubuntu上でGoをインストールします。以下のコマンドで最新バージョンの1.23.4をダウンロードします。

Ubuntu 22.04.5 LTS/Intel 64bitの場合

wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz

その他のバージョンについては以下のサイトを参照してください。

ダウンロードしたファイルを/usr/localに展開します。

tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz
既にGoがインストールされている場合

インストールされているGoのバージョンを確認します。

go version

/usr/local/goにインストールされている古いバージョンのGoを削除し、最新バージョンを展開します。

rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz

/usr/local/go/bin$PATHに追加します。$HOME/.profileをVSCodeで開きパスを追加します。

code $HOME/.profile

以下の行を追加してください。

export PATH=$PATH:/usr/local/go/bin

バージョンを確認します。

go version

go version go1.23.4 linux/amd64と表示されればインストール成功です。

Hello World

作業ディレクトリgolang-introを作成し、main.goを作成します。

main.goに以下のコードを記述します。

package main

import "fmt"

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

実行方法

Goはコンパイル言語なので、先にコンパイルを行い実行用のファイルを作成する必要があります。

go runコマンドを使うことで「コンパイル」と「実行」を同時に行うことができます。

go run main.go

Hello, World!と表示されれば成功です。

Goの基礎文法

押さえておきたいGoの基礎文法を簡単に紹介します。

パッケージ宣言

すべてのGoファイルはパッケージ宣言から始まり、mainパッケージがプログラムの開始点です。

package main

インポート宣言

外部パッケージを利用するための宣言です。複数のパッケージをインポートする場合は()で囲みます。

import (
  "fmt"
  "time"
  "os"
  _ "github.com/go-sql-driver/mysql"
)

_はブランク識別子と呼ばれるものでインポートしたパッケージと依存関係のあるパッケージをインポートする際に使われます。

Goでは、インポートしたパッケージをコード内で使わない場合はコンパイルエラーとなってしまうので依存関係を解決するためのインポートであることを明示しなければなりません。

主なデータ型

Goの主なデータ型は以下の通りです。

データ型説明
bool真偽値true, false
string文字列"Hello, World!"
int整数100
float64浮動小数点数3.14
byteバイト, 符号なし8ビット整数 uint8 の別名'A'

変数宣言

変数の宣言と同時に特定の値で初期化するには次のように記述します。

var 変数名 データ型 = 初期値

var message string = "Hello, World!"
var number int = 100
var isTrue bool = true

Goは型推論が可能なので、データ型を省略することもできます。

var message = "Hello, World!"
var number = 100
var isTrue = true

:=を使ってvarを省略することもできます。(Goではこの書き方がよく使われるため慣れておきましょう)

message := "Hello, World!"
number := 100
isTrue := true

配列とスライス

Goの配列は宣言した時点で型とサイズが決められます。それに対して、スライスはサイズは決められておらず必要に応じて要素を追加できます。

スライスに要素を追加するにはappend関数を使います。

// 配列
numbers := [3]int{1, 2, 3}
fmt.Println(numbers) // [1 2 3]
fmt.Println(numbers[0]) // 1

// スライス
foods := []string{"apple", "banana", "orange"}

// 要素の追加
foods = append(foods, "grape")

fmt.Println(foods) // [apple banana orange grape]

map

マップはキーと値のペアを持つデータ構造です。

map[キーのデータ型]値のデータ型で宣言します。

prices := map[string]int{
    "apple":  100,
    "banana": 200,
    "orange": 150,
}

// bananaの値を取得
fmt.Println(prices["banana"]) // 200

補足:GoのmapはHashMapでありSetの構文はないためmap[string]interface{}を使うことでSetを実現することができます。

tuple

関数の戻り値が複数の場合、複数の値を返すことができます。

func getValues() (int, int) {
 return 1, 2
}

a, b := getValues()
fmt.Println(a, b) // 1 2

nilとゼロ値

Goにおけるnilとゼロ値は異なる目的で使用されますがどちらも「初期化」や「未設定」を意味する値です。

Goにおけるnilは、ポインタ、スライス、マップ、チャネル、インターフェースといった特定のデータ型で「無効」または「未設定」を意味する値として使用されます。

https://ittrip.xyz/go/go-nil-zero-valuesより引用

ゼロ値は変数が初期化されていない場合に自動的に割り当てられる値です。例えば、数値型であれば0、文字列型であれば""、ブール型であればfalseです。

Goの関数では通常、戻り値としてエラーを返し、エラーかどうかを判定するためにnilを使います。

// text.txtファイルを開く
file, err := os.Open("./text.txt")
if err != nil {
    fmt.Println("ファイルを開くことができませんでした") // エラーが発生した場合はerrはnilではない
    return
} else {
    fmt.Println("ファイルを開くことができました")
}

MySQLとは

MySQLは世界で最も人気があり、広く使われているオープンソースのリレーショナルデータベース(RDBMS)です。

Web研では以前、SQLiteを使ったデータベースの講義を行いました。SQLiteはサーバーレスつまりファイルベースであり、直接ファイルに読み書きを行います。そのため、インストールや設定が不要であり、自己完結型でOSへの依存度が低いのが特徴です。

一方、MySQLはクライアントサーバーモデルに準拠しており、実行するにはサーバーが必要となります。MySQLはプラットフォームに依存しないため、どのOSでも動作し、様々なプログラミング言語と互換性があるのが特徴です。

Web研の第8回の講義内でデータベースに関する詳しい説明があります。

Dockerとは

Dockerはコンテナ型の仮想環境を作成・実行・管理するためのプラットフォームです。

以前にDocker入門講習会を行いましたので、詳しい説明は以下の記事を参照してください。

Dockerのインストール方法

Docker Desktopをインストールします。

Docker Desktopを起動し、Settings -> Resources -> WSL IntegrationからWSL2を有効にします。

DockerでGoとMySQLを繋ぐ

Dockerを用いてGoとMySQLを繋いでみましょう。

作業ディレクトリsample-go-mysql-connect-appを作成し、次の4つのファイルを作成します。

  • Dockerfile (Goのイメージをビルドするためのファイル)
  • docker-compose.yml (GoとMySQLのコンテナを起動するためのファイル)
  • main.go (Goのプログラム)
  • init.sql (MySQLの初期設定)

ファイル階層は以下の通りです。

sample-go-mysql-connect-app
├── Dockerfile
├── docker-compose.yml
├── main.go
└── init.sql

補足:本来はdevelopment/productionを分け、developmentはvolume mountしてrestartだけでサーバーを更新できるようにしたほうが良いです。今回はファイル階層を単純にするためにproduction用のDockerfileを作成します。

Dockerfile
# ベースイメージ
FROM golang:1.23

# ワーキングディレクトリの設定
WORKDIR /app

# モジュールファイルをコピーして依存関係をインストール
COPY go.mod go.sum ./
RUN go mod download

# ソースコードをコピーしてビルド
COPY . .
RUN go build -o main .

# アプリケーションを起動
CMD ["./main"]
docker-compose.yml
version: '3.8'

services:
  mysql:
    image: mysql:8.4
    container_name: sample-go-mysql-connect-app-mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: testdb
      MYSQL_USER: testuser
      MYSQL_PASSWORD: testpassword
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro

  app:
    build: .
    container_name: sample-go-mysql-connect-app
    ports:
      - "8080:8080"
    depends_on:
      - mysql
    environment:
      DB_HOST: mysql
      DB_USER: testuser
      DB_PASSWORD: testpassword
      DB_NAME: testdb

volumes:
  mysql_data:

main.go
package main

import (
 "database/sql" // データベースの操作
 "encoding/json" // JSONのエンコードとデコード
 "fmt"
 "log"
 "net/http" // HTTPでWebサーバーを立てる
 "os"

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

// 構造体を定義
type Item struct {
 ID   int    `json:"id"`
 Name string `json:"name"`
}

func main() {
 // 環境変数からDB接続情報を取得
 dbHost := os.Getenv("DB_HOST")
 dbUser := os.Getenv("DB_USER")
 dbPassword := os.Getenv("DB_PASSWORD")
 dbName := os.Getenv("DB_NAME")

 dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s", dbUser, dbPassword, dbHost, dbName)
 db, err := sql.Open("mysql", dsn)
 if err != nil {
  log.Fatalf("Failed to connect to database: %v", err)
 }
 defer db.Close()

 // itemsにGETリクエストがあった場合の処理
 http.HandleFunc("/items", func(w http.ResponseWriter, r *http.Request) {
  if r.Method != http.MethodGet {
   http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
   return
  }

  // データベースからデータを取得
  rows, err := db.Query("SELECT id, name FROM items")
  // エラーハンドリング
  if err != nil {
   http.Error(w, fmt.Sprintf("Database query failed: %v", err), http.StatusInternalServerError)
   return
  }
  defer rows.Close()

  // 取得したデータを格納するスライスを定義
  var items []Item
  for rows.Next() {
   var item Item
   if err := rows.Scan(&item.ID, &item.Name); err != nil {
    http.Error(w, fmt.Sprintf("Row scan failed: %v", err), http.StatusInternalServerError)
    return
   }
   // 取得したレコードをitemsに追加
   items = append(items, item)
  }

  // JSON形式でレスポンスを返す
  w.Header().Set("Content-Type", "application/json")
  if err := json.NewEncoder(w).Encode(items); err != nil {
   http.Error(w, fmt.Sprintf("JSON encoding failed: %v", err), http.StatusInternalServerError)
  }
 })

 // サーバーを起動
 log.Println("Server started on :8080")
 log.Fatal(http.ListenAndServe(":8080", nil))
}

次に、新しいGoモジュールを初期化するためのコマンドを実行します。

このコマンドで、モジュール名とモジュールが依存する他のモジュールの情報が含まれるgo.modファイルが作成されます。

go mod init sample-go-mysql-connect-app

モジュールの依存関係を整理するために、以下のコマンドを実行します。

go mod tidy
init.sql
CREATE TABLE items (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL
);

INSERT INTO items (name) VALUES ('Item 1'), ('Item 2'), ('Item 3');

以下のコマンドでコンテナを起動します。

docker-compose up --build

http://localhost:8080/itemsにアクセスすると、以下のようにJSON形式でデータが表示されます。

[
    {
        "id": 1,
        "name": "Item 1"
    },
    {
        "id": 2,
        "name": "Item 2"
    },
    {
        "id": 3,
        "name": "Item 3"
    }
]

コンテナを停止するには、以下のコマンドを実行します。

docker-compose down

やってみよう

サンプルコードを基に次のような課題に取り組んでみましょう。

  • postメソッドを追加して、新しいアイテムをデータベースに追加する
  • deleteメソッドを追加して、アイテムを削除する
  • usersテーブルやcommentsテーブルなど新しいテーブルを追加する
  • フロントエンドとバックエンドを繋いでアイテムを追加・削除できるようにする