ハッカソンなど短期間でWebアプリを開発する際のバックエンドのGo実装例です。 学習コストと開発コストを抑えることを目的としています。
GitHubの Use this template ボタンからレポジトリを作成します。
gonew コマンドからでも作成できます。gonew コマンドを使うと、モジュール名を予め変更した状態でプロジェクトを作成することができます。
gonew github.com/ras0q/go-backend-template {{ project_name }}最低限DockerとDocker Composeが必要です。 Compose Watchを使うため、Docker Composeのバージョンは2.22以上にしてください。
Linter, Formatterにはgolangci-lintを使っています。
VSCodeを使用する場合は.vscode/settings.jsonでLinterの設定を行ってください
{
"go.lintTool": "golangci-lint"
}Organizing a Go module - The Go Programming Language などを参考にしています。
$ tree | manual-explain
.
├── main.go # エントリーポイント
├── infrastructure # 統合テスト用に公開するDBやDI等の設定
├── internal # ロジック (結合テストに公開する必要がないもの)
│ ├── handler # APIハンドラ
│ ├── repository # DBアクセス
│ └── services # 外部サービス, 複雑なビジネスロジック
└── integration_tests # 結合テスト特に重要なものは以下の通りです。
アプリケーションのエントリーポイントを配置します。
Tips: 複数のエントリーポイントを実装する場合は、cmd ディレクトリを作成し、各エントリーポイントを cmd/{app name}/main.go に書くと見通しが良くなります。
DBスキーマの定義、DBの初期化、マイグレーションを行っています。
マイグレーションツールはpressly/gooseを使っています。
アプリケーション本体のロジックを配置します。 主に2つのパッケージに分かれています。
handler/: ルーティング- 飛んできたリクエストを裁いてレスポンスを生成する
- DBアクセスは
repository/で実装したメソッドを呼び出す - Tips: リクエストのバリデーションがしたい場合は↓のどちらかを使うと良い
- go-playground/validator: タグベースのバリデーション
- go-ozzo/ozzo-validation: コードベースのバリデーション
repository/: ストレージ操作- DBや外部ストレージなどのストレージにアクセスする
- 引数のバリデーションは
handler/に任せる
- 引数のバリデーションは
- DBや外部ストレージなどのストレージにアクセスする
Tips: internalパッケージは他モジュールから参照されません(参考: Go 1.4 Release Notes)。
依存性注入や外部ライブラリの初期化のみをcore/やpkg/で公開し、アプリケーションのロジックはinternal/に閉じることで、後述のintegration_tests/go.modなどの外部モジュールからの参照を最小限にすることができ、開発の効率を上げることができます。
結合テストを配置します。 APIエンドポイントに対してリクエストを送り、レスポンスを検証します。 短期開発段階では時間があれば書く程度で良いですが、長期開発に向けては書いておくと良いでしょう。
package integration_tests
import (
"testing"
"gotest.tools/v3/assert"
)
func TestUser(t *testing.T) {
t.Run("get users", func(t *testing.T) {
t.Run("success", func(t *testing.T) {
t.Parallel()
rec := doRequest(t, "GET", "/api/v1/users", "")
expectedStatus := `200 OK`
expectedBody := `[{"id":"[UUID]","name":"test","email":"[email protected]"}]`
assert.Equal(t, rec.Result().Status, expectedStatus)
assert.Equal(t, escapeSnapshot(t, rec.Body.String()), expectedBody)
})
})
}Tips: DBコンテナの立ち上げにはory/dockertestを使っています。
Tips: アサーションにはgotest.toolsを使っています。
go test -updateを実行することで、expectedXXXのスナップショットを更新することができます(参考: gotest.toolsを使う - 詩と創作・思索のひろば)。
外部サービス(traQ, Twitterなど)へのアクセスが発生する場合はTest Doublesでアクセスを置き換えると良いでしょう。
開発に用いるコマンド一覧
Tip
xc を使うことでこれらのコマンドを簡単に実行できます。
詳細は以下のページをご覧ください。
go install github.com/joerdav/xc/cmd/xc@latestアプリをビルドします。
CMD=server
go mod download
go build -o ./bin/${CMD} ./main.goホットリロードの開発環境を構築します。
docker compose watchAPI、DB、DB管理画面が起動します。 各コンテナが起動したら、以下のURLにアクセスすることができます。 Compose Watchにより、ソースコードの変更を検知して自動で再起動します。
- http://localhost:8080/ (API)
- http://localhost:8081/ (DBの管理画面)
全てのテストを実行します。
Requires: Test-Unit, Test-Integration
RunDeps: async
echo hello単体テストを実行します。
go test -v -cover -race -shuffle=on ./internal/...結合テストを実行します。
[ ! -e ./go.work ] && go work init . ./integration_tests
go test -v -cover -race -shuffle=on ./integration_tests/...結合テストのスナップショットを更新します。
[ ! -e ./go.work ] && go work init . ./integration_tests
go test -v -cover -race -shuffle=on ./integration_tests/... -updateLinter (golangci-lint) を実行します。
golangci-lint run --timeout=5m --fix ./...長期開発に向けた改善点をいくつか挙げておきます。
- ドメインを書く (
internal/domain/など)- 現在は簡単のためにAPIスキーマとDBスキーマのみを書きこれらを直接やり取りしている
- 本来はアプリの仕様や概念をドメインとして書き、スキーマの変換にはドメインを経由させるべき
- クライアントAPIスキーマを共通化させる
- OpenAPIやGraphQLを使い、そこからGoのファイルを生成する
- 単体テスト・結合テストのカバレッジを上げる
- ログの出力を整備する
- ロギングライブラリは好みに合ったものを使うと良い
GitHub Actions でビルドした Docker image を Heroku や NeoShowcase のような PaaS にデプロイするためのJobを用意しています。
詳しくは ./.github/workflows/image.yaml を参照してください。
不要なファイルがDocker imageに混入するのを防ぐために .dockerignore をallowlist方式にしています。
ビルドに必要なファイルやディレクトリを追加した場合、 .dockerignore も編集してください。
DiscussionにQ&A を用意しています。
ここに @ras0q へのメンションを付けて質問を投げてくれれば答えます。
その他SNSなどの手段での質問ももちろん可能ですが、全体に公開されているDiscussionを推奨しています。