🕟

【Gin+Docker】Railsライクなディレクトリ構成で簡単なアプリを作ってみる (1)環境構築/Hello,world編

2023/06/29に公開

最近Go+Ginについて勉強していて、自分自身の備忘録的なノリで書いています。
結構我流なところあるので、間違っている部分等あれば指摘していただければ幸いです。

想定読者

  • とりあえずGinを使ってみたいと思っている人

今回の最終目標

以下のような簡単なアプリを作ります。

  • ユーザ名&パスワードでユーザ登録ができる
  • ログインしたユーザがタイトル&本文で記事投稿ができる
  • 以上の2つをGUIで行える。

記事構成

各トピックごとに分けて投稿していこうと思います。

  1. 環境構築/Hello,world編(本記事)
  2. ルーティング/controller編
  3. database設定/model編
  4. view編
  5. middleware編

最終的なディレクトリ構成

全記事が終わった時点で、このようなディレクトリ構成になる予定です。なお、ファイルについては省略しています。

root/
├ docker/
│    └ go/                            # docker関係 
└ src/
   ├ app/
   │   ├ assets/
   │   │   ├ images/      # ロゴなどの画像置き場
   │   │   └ stylesheets/ # CSS置き場
   │   ├ controllers/    # コントローラ
   │   ├ middlewares/    # ミドルウェア
   │   ├ models/         # モデル
   │   └ templates/           # views用テンプレートファイル
   ├ config/                      # サービス起動時に読み込みたい設定ファイル置き場
   └ database/                  # データベース接続処理、SQL系の記述など

前提条件

dockerおよびdocker-composeコマンドが動く環境である

環境構築

条件

今回、以下の項目を満たすように環境を構築していきたいと思います。

  • dockerを使用し、ローカルのGoの環境を汚さない
  • go.sumおよびgo.modは初回のみ作成し、以後は作成しない
  • ホットリロードできるようにする

Dockerfile

まず今回使うDockerfileを作っていきます。docker/go/ディレクトリに配置します。

docker/go/Dockerfile
FROM golang:1.20-alpine

ENV WORKDIR /go/src
WORKDIR $WORKDIR

COPY src/go.mod go.mod
COPY src/go.sum go.sum

ENTRYPOINT ["sh", "start.sh"]

dockerイメージは記事執筆時点で最新の1.20系かつ軽量のalpineを使います。

WORKDIRを/go/srcに設定しておきます。これで今後のコマンドは/go/srcで実行されるようになります。

初回のみ実行したいgo mod initだけ実行しておき、ENTRYPOINTには、後ほど作るstart.shを実行するように設定しておきます。

docker-compose.yml

次に、docker-compose.ymlを作ります。このファイルはプロジェクトディレクトリ直下に配置します。

docker-compose.yml
version: '3'
services:
    go:
        build:
            context: .
            dockerfile: docker/go/Dockerfile
        volumes:
            - "./src:/go/src"
        ports:
            - 8080:8080

ここでのポイントは以下の点です。

  • 先ほど作成したDockerfileを使ってビルドする
  • ポートフォワードでホストのポート8080とコンテナの8080を繋げる
  • プロジェクト以下の/srcディレクトリをコンテナの/go/srcにマウントする

start.sh

dockerコンテナ起動時に実行するコマンドを記述します。このファイルはsrc/ディレクトリに配置します。

src/start.sh
#! /bin/sh

go mod tidy
go run main.go

go mod tidyおよびgo run main.goは起動時は毎回実行したいので、Dockerfileではなくこちらに記述します。
なお、start.shのパーミッションが644などで実行ができない場合は、$ sudo chmod 777 src/start.shを実行するなどしてパーミッションを書き換えておきます。

Hello, world!を表示する

main.go

とりあえずhttp://localhost:8080にアクセスするとHello, world!を返すだけのmain.goを作ります。このファイルはsrc/ディレクトリに配置します。

src/main.go
package main

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

func main() {
	engine := gin.Default()

	engine.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "Hello, world!")
	})

	engine.Run(":8080")
}

今回使うフレームワークのGinおよびhttpクライアントなどの処理が使えるnet/httpパッケージをインポートしています。

gin.Default()*gin.Engine型のインスタンスengineを作ります。これはルーティングや、テンプレートHTML、静的な画像やcssなどのアセットの登録などが行える重要なインスタンスです。

次にengine.GET("/", ...)/にGETメソッドでアクセスした際のルーティングを登録しています。
*gin.Context型のインスタンスcは、リクエストパラメータなどの情報を取得する際に使用します。
今回はHello, world!の文字列を返すだけにしています。

最後にengine.Run(":8080")で、8080番ポートを使用してサーバにアクセスできるようにします。

実行

ここまで進めたところで、ファイルはそれぞれ以下のように配置されていると思います。

root/
├ docker/
│    └ go/
│        └ Dockerfile
├ src/
│    ├ main.go
│  └ start.sh
└ docker-compose.yml

$ cd src && go mod init gin_sample && go mod tidy && cd ..を実行し、src/ディレクトリにgo.sumおよびgo.modを作成します。

$ docker-compose upコマンドで実行し、うまくいけばブラウザでlocalhost:8080にアクセスするとHello, world!の文字列が表示されます。

ホットリロードを有効にする

このままでは、ホットリロードが有効になっていません。そのため、dockerコンテナ起動中にコードを書き換えても変更は反映されません(main.goのHello, world!の文言を変えると確認できると思います)。

開発時、変更がある度にいちいちdockerコンテナを落として立ち上げ直して、などとやるのは手間なので、airというパッケージを使ってホットリロードするようにします。

air.toml

まず、airのサンプル設定ファイルがあるので、それをまるっとコピーしてsrc/air.tomlとして保存しておきます。
https://github.com/cosmtrek/air/blob/master/air_example.toml

Dockerfile

docker/go/Dockerfileを以下のように変更して、airのinstallを追加します。

docker/go/Dockerfile
FROM golang:1.22-alpine # 1.20から1.22に変更

ENV WORKDIR /go/src
WORKDIR $WORKDIR

COPY src/go.mod go.mod
COPY src/go.sum go.sum

RUN go install github.com/cosmtrek/air@latest # 追加

ENTRYPOINT ["sh", "start.sh"]

start.sh

起動コマンドをairに変更します。

src/start.sh
#! /bin/sh

air air.toml

実行

$ docker-compose upを実行し、サーバを起動します。
起動中にmain.goのHello, world!Hello, hoge!等に変更したのち、ブラウザのページを更新して表示される文字列が変わっていれば成功です!

コンテナ外からの変更だとホットリロードがうまくいかない

筆者の環境では、普通にVSCode上でコードを変更しても、airが変更を認識せずホットリロードがうまく動いてくれませんでした。

いろいろ調べたら、コンテナ内での変更であればうまくいくようでした。
なので、VSCodeの拡張Remote Explorerを使って、コンテナにアタッチすることでVSCodeを使いつつホットリロードできるようになりました。

(2024/4/10追記)
筆者はM1 Macbook上でcolimaを使ってdockerを動かしており、その環境だとairのホットリロードがうまく効かなかったようです。
2023年12月にこちらの問題は解消し、air.tomlでpoll=trueにすることによりホットリロードが効くようになりました!

https://github.com/cosmtrek/air/issues/420

終わりに

ここまでのコードは以下に置いています。
リポジトリ
今回のコミット
次の記事は、ルーティング/controller編を予定しています。

ここまで読んでくださりありがとうございました。

参考

Discussion