🛠️

GolangによるWebアプリのローカル開発環境サンプル(Docker, Gin)

2021/12/05に公開

4 日目の記事は@shotanueさんのJetBrains Gateway を SSM 経由で EC2 インスタンスに接続して、IDE(IntelliJ IDEA,PhpStorm, Go Land...)を使うでした。
6 日目の記事は@poemnさんの予定です。

記事の目的

現職への転職をきっかけに初めて業務で Go(Golang)を使用することになったので、学習がてら Go を使って Web アプリを作成してみることにしました。
それに伴ってローカル開発環境の構築を行ったので、備忘録と共有を兼ねてそれらの解説記事を書きました。

下記の要素を解説しています。

  • 採用技術
  • ディレクトリ構成
  • 開発環境の設定詳細
  • 開発する際の操作(コマンド等)
  • 動くものを触ってみる

構成概要

今回は Web ブラウザからアクセスして利用できる Web アプリの作成を想定します。
ただしフロントエンドは Next.js で別途構築して、Go を用いたバックエンドは WebAPI として疎結合にする想定です。
そのため、フロントエンドの開発に関してここでは考慮していません。
ただ Go によるレンダリングでフロントエンドの実装を含めるとしても、ほとんど同じ環境で開発できる想定です。

ソースコードは GitHub に上げてます。
https://github.com/datsukan/go-app-sample

採用技術

仮想化環境

Docker

定番のコンテナ仮想化技術です。
ホストマシンの OS に合わせてインストールして使用します。
設定自体は基本的にホストマシンの OS に依存しません。
複数のプロセスを連携させるのに docker-compose を利用します。

採用理由は

  • 使い慣れている
  • コードで環境を管理できる
  • コンテナは再構築がめっちゃ手軽
  • マルチプラットフォーム
  • 本番環境の構築にも利用できる

RDBMS

MariaDB

ざっくり言うとライセンス違いの MySQL です。
MySQL と互換性が維持され続けているので通常利用であれば違いを意識することはないです。
RDBMS としてアプリケーションのデータを永続化および管理します。

採用理由は、

  • MySQL および MariaDB は使い慣れている
  • 仕様の僅かな違いで PostgreSQL より個人的に好き(テーブルのコメントとか)
  • MySQL は Oracle の手中なので、気持ち的に GPL ライセンスの MariaDB 使いたい

Web フレームワーク

Gin

WebAPI 用のマイクロフレームワークです。
ネット上での情報量が多く、国内でも人気高めのようです。

採用理由は

  • 今回は WebAPI だけの機能で充分
  • 人気で情報が豊富(巨人の肩に乗りたい)

ライブラリ

ライブリロード

Air

ライブリロードとはコードに変更があった場合に検知して、自動的にビルドなどの反映を行うことです。
フロントエンド開発で言うところのホットリロードとほぼ同義ですね。
Air は Go のコードで変更が保存された場合に自動的にビルドしてくれます。
なので開発しながら挙動を確認するのがとっても楽です。

採用理由は

  • 開発が活発
  • ネット上で日本語の情報量が多い
  • 作者が Gin を意識して作ったみたいなので相性良さそう

デバッガー

Delve

コードエディタや IDE でステップ実行したり実行途中の変数値を確認といったデバッグを行うためのライブラリです。

採用理由は

  • Air と組み合わせて簡単に設定できる
  • ネット上で日本語の情報量が多い(というかそれ以外が少なすぎ…?)

ORM

GORM

データベースの操作を行うための ORM です。
ORM として求められるほとんどは備えており、よくあるメソッドチェーンでクエリを記述できるので書きやすいと思います。
Go では定番のようです。

採用理由は

  • リッチな ORM で楽したい
  • 人気でネット上の情報が豊富(巨人の肩に乗りたい)

Migration

golang-migrate

CLI から migration ファイルを作成したり、データベースに対して migration を実行するライブラリです。
データベース上のテーブルの新規作成・変更・削除などをコードで定義・管理できます。
migration ファイル自体は普通の SQL ファイルなのですが、データベースに専用のテーブルを作成して実行有無や順序を管理しています。
そのため変更差分だけを反映したり、特定の変更までをロールバックすることが可能です。

採用理由は

  • 最低限欲しい機能がある
  • 機能や使い方がシンプルで分かりやすい
  • それなりにメンテされている
  • ネット上で日本語の情報量が多い

環境変数管理

GoDotEnv

env ファイルから環境変数を読み込むためのライブラリです。
godotenv.Load('.env')で env ファイルから読み込むと Go の標準ライブラリである os パッケージを用いてos.Getenv('HOGE')で値を取得できます。

採用理由は

  • 検索して一番情報が多かった
  • 導入や理由がかなりシンプルで分かりやすい

ディレクトリ構成

あくまで私が個人的に開発する構成として考えた場合なので、柔軟に読み取ってください。
細かいアーキテクチャの話はここではしません。
詳細は GitHub のソースを見てください。
https://github.com/datsukan/go-app-sample

./
├── .vscode                             # Visual Studio Codeの設定ファイルを配置するディレクトリ
├── back                                # バックエンドのソースを配置するディレクトリ
│   ├── app                             # アプリケーションロジックのソースを配置するディレクトリ
│   │   ├── domain                      # クリーンアーキテクチャにおけるdomain層のソースを配置するディレクトリ
│   │   ├── infrastructure              # クリーンアーキテクチャにおけるinfrastructure層のソースを配置するディレクトリ
│   │   ├── interfaces                  # クリーンアーキテクチャにおけるinterface層のソースを配置するディレクトリ
│   │   │   ├── controllers
│   │   │   └── database
│   │   └── usecase                     # クリーンアーキテクチャにおけるusecase層のソースを配置するディレクトリ
│   ├── go.mod                          # Goの依存モジュール管理用のファイル
│   ├── go.sum                          # Goの依存モジュールのチェックサムの管理用のファイル
│   ├── main.go                         # すべての始まり
│   ├── migrations                      # Migration(golang-migrateによって生成・実行されるデータベースの定義)のソースを配置するディレクトリ
│   ├── README.md
│   ├── seeds                           # Seed(コードによるデータベースのデータ生成定義)のソースを配置するディレクトリ
│   └── tests                           # テストコードを配置するディレクトリ
├── docker                              # DockerfileやDockerコンテナにマウントする設定ファイル等を配置するディレクトリ
│   └── go                              # Goに関連するDockerfileやDockerコンテナにマウントする設定ファイル等を配置するディレクトリ
│       └── Dockerfile
└── docker-compose.yml                  # Dockerの環境全体を紐付けおよび定義するファイル

各種設定

環境を構成しているソフト・ツールの設定内容を説明します。
この設定と main.go だけあれば最低限動くものになります。

Docker

仮想環境を立ち上げるために必要なので一番コアになるものです。

docker-compose.yml

アプリケーションサーバーとして Go のコンテナ、RDBMS として MariaDB のコンテナが起動して、通信およびデータの永続化を行うようにしています。~~~~

version: '3'

services:
  go:
    container_name: sample-go
    platform: linux/x86_64
    hostname: localhost
    ports:
      - 80:80
      - 2345:2345
    build:
      context: .
      dockerfile: ./docker/go/Dockerfile
    volumes:
      - ./back:/go/src/app
    networks:
      - common
    security_opt:
      - apparmor:unconfined
    cap_add:
      - SYS_PTRACE

  mariadb:
    container_name: sample-mariadb
    image: mariadb:10
    platform: linux/x86_64
    hostname: localhost
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: sample
    volumes:
      - sample-dbdata:/var/lib/mysql
    networks:
      - common

volumes:
  sample-dbdata:
    driver: local

networks:
  common:
    driver: bridge

docker/go/Dockerfile

Go のコンテナに細かく設定したい内容を記述しています。
ここではイメージ作成時に各種ライブラリのインストール、依存モジュールのインストール、コンテナ立ち上げ時に Air を起動するように設定しています。

FROM golang:1.17.2-alpine


WORKDIR /go/src/app/
COPY ./back .

RUN apk upgrade --update && \
    apk --no-cache add git gcc musl-dev

RUN go get github.com/gin-gonic/gin \
    github.com/jinzhu/gorm \
    github.com/jinzhu/gorm/dialects/mysql \
    github.com/joho/godotenv

RUN go mod tidy

RUN go get -u github.com/cosmtrek/air && \
    go build -o /go/bin/air github.com/cosmtrek/air

RUN go get -u github.com/go-delve/delve/cmd/dlv && \
    go build -o /go/bin/dlv github.com/go-delve/delve/cmd/dlv

RUN go install -tags 'mysql' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

CMD ["air", "-c", ".air.toml"]

Air

back/.air.toml

ライブリロードを行う Air の設定です。
ほぼデフォルトのままですが、Delve を使ってデバッグができるようにしてあります。

root = "."
tmp_dir = "tmp"

[build]
bin = "./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor"]
exclude_file = []
exclude_regex = []
exclude_unchanged = false
follow_symlink = false
full_bin = "APP_ENV=dev APP_USER=air dlv exec ./tmp/main --accept-multiclient --log --headless --continue --listen :2345 --api-version 2"
#full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true

[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"

[log]
time = false

[misc]
clean_on_exit = false

開発時の操作

GitHub に上げているサンプルコードの場合はこれらの操作が可能です。

仮想環境の起動

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

バックグラウンド実行の場合

$ docker-compose up -d

これだと各種プロセスが吐くログは見れません。

フォアグラウンド実行の場合

$ docker-compose up

これだと標準出力で各種プロセスのログが表示されます。

仮想環境の停止

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker-compose stop

デバッグ

VSCode の場合はデバッガーの設定ファイルをソースに含めているので、デバッガーを起動して普通に操作するだけでブレイクポイントによるステップ実行などが可能です。

Migration

詳細は golang-migrate のリファレンスなどを参照してください。

https://github.com/golang-migrate/migrate
https://dev.classmethod.jp/articles/db-migrate-with-golang-migrate/

新規定義ファイル作成

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker exec -it sample-go sh
$ migrate create -ext sql -dir migrations -seq create_xxxx_table

1 つめのコマンドで go のコンテナに接続します。
2 つ目のコマンドで定義ファイルを作成します。

定義の反映

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker exec -it sample-go sh
$ migrate -database 'mysql://root:password@tcp(sample-mariadb:3306)/sample' -path migrations up

1 つめのコマンドで go のコンテナに接続します。
2 つ目のコマンドで未反映の定義がすべてデータベースへ反映されます。

反映済み定義をロールバック

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker exec -it sample-go sh
$ migrate -database 'mysql://root:password@tcp(sample-mariadb:3306)/sample' -path migrations down

1 つめのコマンドで go のコンテナに接続します。
2 つ目のコマンドですべての反映済み定義をデータベースからロールバックします。
※down の定義ファイルの内容を実行しているだけ

Seed

新規定義作成

back/seeds/seeds/users.goを参考にデータの登録処理を作成してください。
back/seeds/seeds/seeds.goの All メソッドが返す配列に、上記で実装した処理を用いたデータ登録ケースを追加してください。

定義の反映

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker exec -it sample-go sh
$ go run seeds/seeder.go

API の手動実行

VSCode で拡張機能の REST Client をインストールすると、back/tests/manual/user.httpのようなリクエスト定義をその場で実行可能になります。
手軽にレスポンスを確認できるので開発時に便利です。

SQL クライアントソフトで DB に接続

Docker コンテナを起動した状態で下記の情報を使って接続可能です。

項目
hostname localhost
port 3306
username root
password password
database sample

動くものを触ってみる

色々書きましたが動かしてみないとよくわからない場合もあると思います。
GitHub に上げているサンプルコードを動かす一連の流れを確認してみます。

  1. Docker を起動する
  2. コンテナを起動する
  3. migration を反映する
  4. seed を反映する
  5. リクエストを送信する
  6. SQL クライアントでテーブルを確認する

1. Docker を起動する

Docker for Windows なり Docker for mac なりを起動してください。

2. コンテナを起動する

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker-compose up -d

3. migration を反映する

プロジェクトのルートディレクトリで下記のコマンドを実行してください。

$ docker exec -it sample-go sh
$ migrate -database 'mysql://root:password@tcp(sample-mariadb:3306)/sample' -path migrations up

4. seed を反映する

プロジェクトのルートディレクトリで下記のコマンドを実行してください。
3 の 1 つ目のコマンドでコンテナに接続しているため、切断していなければ下記の 1 つ目のコマンドは実行不要です。

$ docker exec -it sample-go sh
$ go run seeds/seeder.go

5. リクエストを送信する

VSCode の拡張機能である REST Client をインストールした状態でback/tests/manual/user.httpを開いてください。
###の下に表示されているSend Requestをクリックするとリクエストが送信されます。

まず「全ユーザー情報を取得する」を実行してみてください。
seed を反映したことで登録されたユーザー 2 人分の情報がレスポンスされるはずです。

次に「特定のユーザー情報を取得する」を実行してみてください。
id=1 のユーザー 1 人分の情報がレスポンスされるはずです。

次に「指定したユーザー情報を登録する」を実行してみてください。
既に登録済みの 2 人以外にリクエストしたユーザーが追加されたレスポンスになるはずです。

6. SQL クライアントでテーブルを確認する

SQL クライアント(※)で DB に接続して、users テーブルを参照してください。
そうすると 3 名のユーザーのレコードが存在しているはずです。
また、レコードを更新すると「全ユーザー情報を取得する」や「特定のユーザー情報を取得する」の結果が変化するはずです。

※なんでもいいですが私は DBeaver を使っています

ちょっと解説

リクエストされた際の処理の流れとしては

  1. back/main.goの main メソッドに処理が到達する
  2. 各種設定が読み込まれる
  3. back/app/infrastructure/Routing.goでリクエストのパス・メソッドに合わせてコントローラーをのアクションメソッドを実行する
  4. コントローラー内でユースケースの処理を実行する
  5. ユースケースでリポジトリの処理を実行して DB の参照や更新を行う

おわり

なかなか悪くない開発環境になりました。
開発を進める中でブラッシュアップしていこうと思います。

GitHubで編集を提案

Discussion