Chapter 03

第2章 プロジェクト構成とアーキテクチャ

kakkky
kakkky
2025.02.07に更新

この章で行うこと

コードはあまりいじりませんが以下のことを理解していきます。

  • プロジェクト構成からTodo APIの全体像を把握する
  • アーキテクチャ原則の概要を掴む
  • マルチモジュールの依存関係を解決する(Workspacesモード)

アーキテクチャ概要図

プロジェクト構成

まず、APIが完成した時のディレクトリを以下に示します。

完成時のディレクトリ(全て)
.
├── .env
├── .github
│   └── workflows
│       └── test.yml
├── .gitignore
├── Dockerfile.app
├── Makefile
├── app
│   ├── .air.toml
│   ├── adapter
│   │   ├── presentation
│   │   │   ├── handler
│   │   │   │   ├── auth
│   │   │   │   │   ├── login_handler.go
│   │   │   │   │   ├── login_models.go
│   │   │   │   │   └── logout_handler.go
│   │   │   │   ├── health
│   │   │   │   │   ├── handler.go
│   │   │   │   │   └── health_models.go
│   │   │   │   ├── task
│   │   │   │   │   ├── delete_task_handler.go
│   │   │   │   │   ├── get_task_handler.go
│   │   │   │   │   ├── get_task_models.go
│   │   │   │   │   ├── get_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_handler.go
│   │   │   │   │   ├── get_user_tasks_models.go
│   │   │   │   │   ├── post_task_handler.go
│   │   │   │   │   ├── post_task_models.go
│   │   │   │   │   ├── update_task_state_handler.go
│   │   │   │   │   └── update_task_state_models.go
│   │   │   │   └── user
│   │   │   │       ├── delete_user_handler.go
│   │   │   │       ├── get_users_handler.go
│   │   │   │       ├── get_users_models.go
│   │   │   │       ├── post_user_handler.go
│   │   │   │       ├── post_user_models.go
│   │   │   │       ├── update_user_handler.go
│   │   │   │       └── update_user_models.go
│   │   │   ├── middleware
│   │   │   │   ├── authorization.go
│   │   │   │   └── logger.go
│   │   │   └── presenter
│   │   │       ├── failure.go
│   │   │       └── success.go
│   │   ├── queryservice
│   │   │   ├── interface_querier.go
│   │   │   └── task_queryservice.go
│   │   └── repository
│   │       ├── interface_kvs_commander.go
│   │       ├── interface_querier.go
│   │       ├── jwt_authenticator_repository.go
│   │       ├── task_repository.go
│   │       └── user_repository.go
│   ├── application
│   │   └── usecase
│   │       ├── auth
│   │       │   ├── authorization_usecase.go
│   │       │   ├── authorization_usecase_dto.go
│   │       │   ├── authorization_usecase_test.go
│   │       │   ├── interface_jwt_authenticator.go
│   │       │   ├── interface_jwt_authenticator_repository.go
│   │       │   ├── login_usecase.go
│   │       │   ├── login_usecase_dto.go
│   │       │   ├── login_usecase_test.go
│   │       │   ├── logout_usecase.go
│   │       │   ├── logout_usecase_dto.go
│   │       │   ├── logout_usecase_test.go
│   │       │   ├── mock_jwt_authenticator.go
│   │       │   └── mock_jwt_authenticator_repository.go
│   │       ├── task
│   │       │   ├── create_task_usecase.go
│   │       │   ├── create_task_usecase_dto.go
│   │       │   ├── create_task_usecase_test.go
│   │       │   ├── delete_task_usecase.go
│   │       │   ├── delete_task_usecase_dto.go
│   │       │   ├── delete_task_usecase_test.go
│   │       │   ├── fetch_task_usecase.go
│   │       │   ├── fetch_task_usecase_dto.go
│   │       │   ├── fetch_task_usecase_test.go
│   │       │   ├── fetch_tasks_usecase.go
│   │       │   ├── fetch_tasks_usecase_test.go
│   │       │   ├── fetch_user_tasks_usecase.go
│   │       │   ├── fetch_user_tasks_usecase_dto.go
│   │       │   ├── fetch_user_tasks_usecase_test.go
│   │       │   ├── interface_task_queryservice.go
│   │       │   ├── mock_task_queryservice.go
│   │       │   ├── update_task_state_usecase.go
│   │       │   ├── update_task_state_usecase_dto.go
│   │       │   └── update_task_state_usecase_test.go
│   │       └── user
│   │           ├── fetch_users_usecase.go
│   │           ├── fetch_users_usecase_dto.go
│   │           ├── fetch_users_usecase_test.go
│   │           ├── register_usecase.go
│   │           ├── register_usecase_dto.go
│   │           ├── register_usecase_test.go
│   │           ├── unregister_usecase.go
│   │           ├── unregister_usecase_dto.go
│   │           ├── unregister_usecase_test.go
│   │           ├── update_profile_usecase.go
│   │           ├── update_profile_usecase_dto.go
│   │           └── update_profile_usecase_test.go
│   ├── config
│   │   ├── config.go
│   │   └── config_test.go
│   ├── docs
│   │   ├── docs.go
│   │   ├── swagger.json
│   │   └── swagger.yaml
│   ├── domain
│   │   ├── errors
│   │   │   ├── errors.go
│   │   │   └── errors_test.go
│   │   ├── task
│   │   │   ├── interface_task_repository.go
│   │   │   ├── mock_task_repository.go
│   │   │   ├── task.go
│   │   │   ├── task_content.go
│   │   │   ├── task_state.go
│   │   │   └── task_test.go
│   │   └── user
│   │       ├── interface_user_domain_service.go
│   │       ├── interface_user_repository.go
│   │       ├── mock_user_domain_service.go
│   │       ├── mock_user_repository.go
│   │       ├── user.go
│   │       ├── user_domain_service.go
│   │       ├── user_domain_service_test.go
│   │       ├── user_email.go
│   │       ├── user_hashed_password.go
│   │       ├── user_test.go
│   │       └── users.go
│   ├── go.mod
│   ├── go.sum
│   ├── infrastructure
│   │   ├── api_test
│   │   │   ├── integration
│   │   │   │   ├── api_test.go
│   │   │   │   ├── auth_api_test.go
│   │   │   │   ├── task_api_test.go
│   │   │   │   └── user_api_test.go
│   │   │   ├── testdata
│   │   │   │   ├── fixtures
│   │   │   │   │   ├── tasks.yml
│   │   │   │   │   └── users.yml
│   │   │   │   └── golden
│   │   │   │       ├── auth
│   │   │   │       │   ├── login_nomal.golden.json
│   │   │   │       │   ├── login_seminomal_email_notfound.golden.json
│   │   │   │       │   ├── login_seminomal_password_mismatch.golden.json
│   │   │   │       │   └── logout_seminomal_not_loggedin.golden.json
│   │   │   │       ├── task
│   │   │   │       │   ├── delete_task_seminomal_forbidden_operate_others_task.golden.json
│   │   │   │       │   ├── delete_task_seminomal_not_found.golden.json
│   │   │   │       │   ├── delete_task_seminomal_not_loggedin.golden.json
│   │   │   │       │   ├── get_task_nomal.golden.json
│   │   │   │       │   ├── get_task_seminomal_not_found.golden.json
│   │   │   │       │   ├── get_task_seminomal_not_loggedin.golden.json
│   │   │   │       │   ├── get_tasks_nomal.golden.json
│   │   │   │       │   ├── get_tasks_seminomal_not_loggedin.golden.json
│   │   │   │       │   ├── get_user_tasks_nomal.golden.json
│   │   │   │       │   ├── get_user_tasks_seminomal_not_loggedin.golden.json
│   │   │   │       │   ├── post_task_nomal.golden.json
│   │   │   │       │   ├── post_task_seminomal_content_empty.golden.json
│   │   │   │       │   ├── post_task_seminomal_invalid_state.golden.json
│   │   │   │       │   ├── post_task_seminomal_not_loggedin.golden.json
│   │   │   │       │   ├── update_task_nomal.golden.json
│   │   │   │       │   ├── update_task_seminomal_forbidden_operate_others_task.golden.json
│   │   │   │       │   ├── update_task_seminomal_invalid_state.golden.json
│   │   │   │       │   ├── update_task_seminomal_not_found.golden.json
│   │   │   │       │   └── update_task_seminomal_not_loggedin.golden.json
│   │   │   │       └── user
│   │   │   │           ├── delete_user_nomal.golden.json
│   │   │   │           ├── delete_user_seminomal_not_loggedin.golden.json
│   │   │   │           ├── get_users_nomal.golden.json
│   │   │   │           ├── get_users_seminomal_not_loggedin.golden.json
│   │   │   │           ├── logout_seminomal_not_loggedin.golden.json
│   │   │   │           ├── post_user_nomal.golden.json
│   │   │   │           ├── post_user_seminomal_dup_email.golden.json
│   │   │   │           ├── update_user_nomal.golden.json
│   │   │   │           └── update_user_seminormal_not_loggedin.golden.json
│   │   │   └── testhelper
│   │   │       ├── format_json.go
│   │   │       ├── nomalize_jwt.go
│   │   │       ├── nomalize_ulid.go
│   │   │       └── setup_login.go
│   │   ├── auth
│   │   │   ├── certificate
│   │   │   │   ├── private.pem
│   │   │   │   └── public.pem
│   │   │   ├── jwt_authenticator.go
│   │   │   ├── jwt_authenticator_test.go
│   │   │   └── key_parse.go
│   │   ├── db
│   │   │   ├── connect.go
│   │   │   ├── container
│   │   │   │   ├── db.go
│   │   │   │   └── dockertest.go
│   │   │   ├── migrations
│   │   │   │   ├── 000001_create_users_table.down.sql
│   │   │   │   ├── 000001_create_users_table.up.sql
│   │   │   │   ├── 000002_change_column_password_to_hashed_password.down.sql
│   │   │   │   ├── 000002_change_column_password_to_hashed_password.up.sql
│   │   │   │   ├── 000003_add_column_timestamp_to_users.down.sql
│   │   │   │   ├── 000003_add_column_timestamp_to_users.up.sql
│   │   │   │   ├── 000004_create_tasks_table.down.sql
│   │   │   │   └── 000004_create_tasks_table.up.sql
│   │   │   ├── mysql
│   │   │   │   └── conf.d
│   │   │   │       └── my.conf
│   │   │   ├── sqlc
│   │   │   │   ├── db.go
│   │   │   │   ├── models.go
│   │   │   │   ├── queries
│   │   │   │   │   ├── tasks.sql
│   │   │   │   │   └── users.sql
│   │   │   │   ├── sqlc.yml
│   │   │   │   ├── sqlc_querier.go
│   │   │   │   ├── tasks.sql.go
│   │   │   │   └── users.sql.go
│   │   │   └── testhelper
│   │   │       └── fixtures.go
│   │   ├── kvs
│   │   │   ├── redis_client.go
│   │   │   ├── redis_commander.go
│   │   │   └── redis_test_client.go
│   │   ├── queryservice_test
│   │   │   ├── queryservice_test.go
│   │   │   ├── task_queryservice_test.go
│   │   │   └── testdata
│   │   │       └── fixtures
│   │   │           ├── tasks.yml
│   │   │           └── users.yml
│   │   ├── repository_test
│   │   │   ├── jwt_authenticator_repository_test.go
│   │   │   ├── repository_test.go
│   │   │   ├── task_repository_test.go
│   │   │   ├── testdata
│   │   │   │   └── fixtures
│   │   │   │       ├── tasks.yml
│   │   │   │       └── users.yml
│   │   │   └── user_repository_test.go
│   │   ├── router
│   │   │   ├── handlers.go
│   │   │   ├── middlewares.go
│   │   │   ├── middlewares_test.go
│   │   │   ├── mux.go
│   │   │   └── mux_test.go
│   │   └── server
│   │       ├── server.go
│   │       └── server_test.go
│   ├── main.go
│   └── tmp
│       ├── build-errors.log
│       └── main
├── compose.yml
├── go.work
├── go.work.sum
└── pkg
    ├── go.mod
    ├── go.sum
    ├── hash
    │   ├── hash.go
    │   └── hash_test.go
    ├── ulid
    │   ├── ulid.go
    │   └── ulid_test.go
    └── validation
        └── validator.go

多すぎて訳わかんないので、ディレクトリだけに絞ります。

.
├── app                               # アプリケーションコード
│   ├── adapter                        # インターフェースアダプター層(外部のフレームワークやライブラリとの接続部分)
│   │   ├── presentation               # プレゼンテーション層(ユーザーとのインタラクションを管理)
│   │   │   ├── handler                # リクエスト処理用ハンドラ(特定の操作に対するハンドリング)
│   │   │   │   ├── auth               # 認証に関連するハンドラ
│   │   │   │   ├── health             # サービスのヘルスチェック用ハンドラ
│   │   │   │   ├── task               # タスクに関連するハンドラ
│   │   │   │   └── user               # ユーザーに関連するハンドラ
│   │   │   ├── middleware             # ミドルウェア(リクエストを事前に処理する層)
│   │   │   └── presenter              # プレゼンター(データを整形して返す)
│   │   ├── query_service              # クエリサービス(データの取得や問い合わせロジック)
│   │   └── repository                 # リポジトリ(データベースとのインタラクション)
│   ├── application                    # ユースケース層(アプリケーションのビジネスロジック)
│   │   └── usecase                    # ユースケース(アプリケーション内の具体的なビジネスルール)
│   │       ├── auth                   # 認証に関するユースケース
│   │       ├── task                   # タスクに関するユースケース
│   │       └── user                   # ユーザーに関するユースケース
│   ├── config                          # 環境設定やコンフィグ管理(設定ファイル等)
│   ├── docs                            # ドキュメント(API仕様や設計書など)
│   ├── domain                          # ドメイン層(ビジネスロジックやエンティティ)
│   │   ├── errors                     # ドメインエラー(ビジネスルールに関連するエラー定義)
│   │   ├── task                       # タスクに関連するドメインロジック
│   │   └── user                       # ユーザーに関連するドメインロジック
│   ├── infrastructure                 # インフラストラクチャ層(外部システムやフレームワークとの連携)
│   │   ├── api_test                   # APIエンドポイントのテスト
│   │   │   ├── integration            # 統合テスト(システム全体の結合テスト)
│   │   │   ├── testdata               # テストデータ
│   │   │   │   ├── fixtures           # 事前準備したデータ
│   │   │   │   └── golden             # ゴールデンデータ(期待する結果データ)
│   │   │   │       ├── auth           # 認証に関するゴールデンデータ
│   │   │   │       ├── task           # タスクに関するゴールデンデータ
│   │   │   │       └── user           # ユーザーに関するゴールデータ
│   │   │   └── testhelper             # テストヘルパー(テストの補助コード)
│   │   ├── auth                       # 認証に関するロジック(例:JWT認証)
│   │   │   └── certificate            # 証明書関連の設定やロジック
│   │   ├── db                         # データベース関連
│   │   │   ├── container              # テスト用DBコンテナ
│   │   │   ├── migrations             # マイグレーション
│   │   │   ├── mysql                  # mysqlの設定
│   │   │   │   └── conf.d             # mysqlの設定ファイル
│   │   │   ├── sqlc                   # ORM
│   │   │   │   └── queries            # SQLクエリ
│   │   │   └── testhelper             # テストヘルパー(DBテスト補助コード)
│   │   ├── kvs                        # キー・バリューストア(Redis)
│   │   ├── queryservice_test          # クエリサービスのテスト
│   │   │   └── testdata               # テストデータ
│   │   │       └── fixtures           # フィクスチャ
│   │   ├── repository_test            # リポジトリのテスト
│   │   │   └── testdata               # テストデータ
│   │   │       └── fixtures           # フィクスチャ
│   │   ├── router                     # ルーティング設定
│   │   └── server                     # HTTPサーバーの設定
│   └── tmp                            # プロジェクトのバイナリファイル
└── pkg                                # 汎用的な処理(ドメインロジックを持たない)
    ├── hash                           # ハッシュ関連処理
    ├── ulid                           # ユニークID生成
    └── validation                     # バリデーション処理

設計ポイントは、

  • クリーンアーキテクチャの考えに基づいた、関心ごとの分離&依存方向の制御
  • ドメイン駆動設計の考えに基づいた、ドメイン知識の表現

にあります。後者については、プロジェクト構成からはわかりづらいので、説明を次章に説明を譲るとして、クリーンアーキテクチャについてはここで触れておきます。

アーキテクチャ

アーキテクチャとは?

書籍では「アーキテクチャとは?」という章で次のように述べられています。

アーキテクチャの形状の目的は、そこに含まれるソフトウェアシステムの開発・デプロイ・運用・保守を容易にすることである。
それらを容易にするための戦略は、できるだけ長い期間、できるだけ多く選択肢を残す事である

出典:Robert C.Martin; 角 征典; 高木 正弘. Clean Architecture 達人に学ぶソフトウェアの構造と設計 (アスキードワンゴ) (p.177). 株式会社ドワンゴ. Kindle 版.

ソフトウェアは文字通り「ソフト」であるべきであり、その「ソフトさ」とは選択肢が多く残されていることで実現されます。
ソフトウェアシステムは、次の二つに分割できると言及されています。

方針

  • ビジネスルール、手順を含む

詳細

  • デバイス、データベース、サーバー等
  • 方針についてやり取りするために必要なもの

システムの「ソフトさ」を実現するのは詳細です。詳細という言葉は、技術的要件と置き換えても齟齬がないのかなと思っています。

つまり、技術的要件の選択肢を多く残すことこそが重要だという事です。

そして、詳細の選択肢を多く残して開発を進めるには、方針と詳細の分離が必要なのです。
詳細の決定・変更は方針に影響を与えてはいけません。

つまり、方針は詳細を把握する(=依存する)べきではないという事です。

それを実現するのが「アーキテクチャ」なんですね!

クリーンアーキテクチャ

「関心事の分離」を目的としたアーキテクチャであり、重要なルールとして、依存性のルールが存在します。

このアーキテクチャは、以下の特性を持つと言及されています。

  • フレームワーク非依存
  • テスト可能性
  • UI非依存
  • データベース非依存
  • 外部エージェント非依存

「非依存」という言葉が連発されています。「非依存」を図っているのはどれも詳細であることにお気づきでしょうか。そう、方針と詳細の分離をしているのですね!

外→内への依存方向の制御

円の外側は仕組み(=詳細)であり、内側は方針とされています。
内側を上位レベルの方針として、依存の方向性は外→内に向いている必要があります。依存という言葉は、コンポーネントの具体的な実装を知っていることと置き換えるとわかりやすいかなと思っています。
つまり、方針は詳細を知るべきではありません。例えば、方針(アプリケーションの振る舞い)は、詳細であるデータベースの変更などに影響を受けません。

図に載っている層の内側から順に見ていきます。

エンティティ

最重要のビジネスルールをカプセル化したものであり、このレイヤーは最も変更されにくいです。なぜなら最上位の方針であるからです。
先ほども似たような例を挙げましたが、フレームワークをginに乗り換えたとて、エンティティレイヤーは外側のことを何も知りませんから、変更などされるはずもないんですね!

ユースケース

アプリケーション固有のビジネスルールが含まれています。インターフェース&アダプターレイヤから値を受け取り、エンティティレイヤーに定義されたロジックを実行します。
つまり、エンティティレイヤーのことは「知っており」、依存しています。しかしながら、このレイヤーも外部の影響を受けません。そのような関心事からは分離されているからです。

インターフェースアダプター

このレイヤーは、「内側」とされるユースケース・エンティティと「外側」にあるフレームワーク&ドライバーレイヤーとの間に位置しています。インターフェース、アダプターという言葉で表現されているように内側と外側、つまり方針と詳細をつなぐ役割を果たしています。外側↔︎内側の間でデータを変換してやり取りを実現させているのです。
例えば、プレゼンテーション(ハンドラ+プレゼンター)は、外側であるHTTPサーバーからJSONを受け取って、データを整形して内側であるユースケースに渡します。図にあるDTOは、のちに取り扱いますがData Transfer Objectの略です。
また、リポジトリは、外側であるデータベースから内側であるエンティティにデータを渡し、ドメインオブジェクトに詰め替えてユースケースで使用可能にしています。

フレームワーク&ドライバー

円の「外側」にあたります。詳細であり、技術的要件にあたる実装が配置されるレイヤーです。
本ハンズオンでは、ここをinfrastructureとして扱い、ディレクトリは以下のようになっています。

infrastructure
├── api_test                   # エンドポイントの結合テスト
│   ├── integration            # 結合テストコード
│   ├── testdata               # テストデータ
│   │   ├── fixtures   
│   │   └── golden            
│   │       ├── auth           
│   │       ├── task   
│   │       └── user           
│   └── testhelper    
├── auth                       # 認証ロジック (JWT)
│   └── certificate            
├── db                         # データベース
│   ├── container              # テスト用DBコンテナ
│   ├── migrations             # マイグレーション
│   ├── mysql                  # MySQLの設定
│   │   └── conf.d           
│   ├── sqlc                   # ORM (sqlc)
│   │   └── queries            
│   └── testhelper             
├── kvs                        # キー・バリューストア (Redis)
├── router                     # ルーティング
└── server                     # HTTPサーバー

認証はJWTというトークンを用いた技術的要件であり、実装の詳細になるためここに配置しています。データベース関連、Redis関連、ルーティング、HTTPサーバーもここに配置しています。

エンドポイントの統合テストに関しては、内容がルーティング〜エンティティレイヤーまで網羅しており、全てのレイヤーに関わっているため、この層においています。

依存関係逆転の原則

これは、依存性のルールを守るために非常に重要な原則です。
👇図も掲載されている、わかりやすい記事がありました。
https://zenn.dev/yoshinani_dev/articles/c743a3d046fa78

のちに、実装の際にも改めて説明を記載しようと思っていますが、クリーンアーキテクチャのレイヤーを用いて説明します。

ユーザーを保存するユースケース(SaveUserUsecase)を例に考えてみましょう。ユーザーを保存するということは、データベースが確実に絡んできますよね。データベースとの仲介役をになっているのがリポジトリです。ということは、ユースケースではリポジトリを利用するということになりそうです。

...依存性のルールを早速破ってしまいました。外→内しか許されていませんでしたね。

ここで、インターフェースの登場です。インターフェースとは、実装の具体ではなく抽象を表すものです。
現段階の話では、SaveUserUsecaseは、Repositoryという具体実装を利用してしまっています。

// 具体実装
type UserRepository struct{}
func (r *UserRepository)Save(){...}

そうではなく、インターフェースを用意して、内側に配置します。インターフェースは、「振る舞い(動作や機能の契約)」を定義するものです。

// 抽象
type UserRepository interface{
	Save()
}

そうすると、SaveUserUsecaseの依存先はUserRepositoryインターフェースとなります。
また、UserRepositoryの具体実装も、UserRepositoryインターフェースを満たすもの捉えることができるため、依存の方向がインターフェースに向きます。

...依存の方向が逆転しました!!

これが依存関係逆転の原則です。

じゃぁ、具体実装はどうなるの?という話になりますが、これは依存性の注入によって実現されます。DI(Dependency Injection)とも言います。

そして、依存性は実行時に注入されます。本ハンズオンでは、ハンドラーオブジェクトを初期化する際にコンストラクタインジェクション(DIを行う方法の一つ)を行い、リポジトリなどの依存性を注入します。DIについては、13章で扱います。

クリーンアーキテクチャについて、なんとなくご理解いただけたでしょうか??
本ハンズオンは、クリーンアーキテクチャに基づいている設計・実装となっているので、次章からコードで改めて理解していきましょう。

ドメイン駆動設計とクリーンアーキテクチャ

本ハンズオンでは、クリーンアーキテクチャにおけるエンティティレイヤーに、ドメイン駆動設計の概念を持ってきています。

ビジネスルールをドメインという最上位レベルの方針を表すモジュールで表現します。

ドメイン駆動設計の文脈上でもっとも重要なことはドメインの隔離を促すことです。すべての詳細がドメインに対して依存するようにすることは、ソフトウェアの方針をもっとも重要なドメインに握らせることを可能にします。

出典:成瀬 允宣. ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 (p.504). 株式会社翔泳社. Kindle 版.

次章でドメイン駆動設計については説明しますが、本ハンズオンのアーキテクチャにおけるドメイン駆動設計の位置付けをなんとなく把握していただければと思います。

(再喝)アーキテクチャ概要図

冒頭に示したこのアーキテクチャ図をなんとなくでも理解できていれば幸いです!
矢印は依存の方向を表しています。

本ハンズオンでは、円の内側からボトムアップで組み立てていきます。ボトムアップに組み立てて実装していくことで、より各層の独立性、依存性のルールを実感できるのではないかと思っています。

マルチモジュールの依存関係を解決する

本ハンズオンでは、アプリケーションコードを含むappモジュールと汎用的処理を含むpkgモジュールに分けて開発していきます。appモジュールがpkgに依存する形となります。このように一つのプロジェクトでローカル環境にモジュールを複数抱えている場合はどのように依存関係を解決すればいいのでしょうか。

選択肢の一つとしては、Workspaces Modeがあげられます。
このセクションでは、pkgモジュールを作成し、ワークスペースにappモジュールとpkgモジュールを入れるところまでを行います。

まずは/pkgディレクトリを作成し、ディレクトリを移動して、go mod initしてください。

% cd pkg 
% go mod init  github.com/kakkky/pkg
go: creating new go.mod: module github.com/kakkky/pkg

この状態では、pkg配下に実装したパッケージをappで利用する、ということはできません。
次に、ルートディレクトリに移動してください。
以下のコマンドを打ちましょう。go work initコマンドの引数にはそれぞれ、モジュールのパスを指定しています。

go work init app pkg  

これで、goはワークスペースモードになりました!
次のファイルが作成されているはずです。

go.work
go 1.23.1

use (
	./app
	./pkg
)

簡易的に試してみましょう。
pkg/hello/hello.goを作成して以下を記述してください。

pkg/hello/hello.go
package hello

import "fmt"

func Hello() {
	fmt.Println("Hello from pkg!")
}

app/main.goに移動して、hello.と打てば補完でpkgモジュールのhelloパッケージを認識し、使用できます。

app/main.go
package main

import (
	"github.com/kakkky/pkg/hello"
)

func main() {
	hello.Hello()
}

docker compose upはタスクとしてMakefileに追加しています。以下のコマンドを打ってコンテナを立ち上げ、ログを確認してみましょう。

make up
todo-api  | !exclude tmp
todo-api  | building...
todo-api  | running...
todo-api  | Hello from pkg!

https://go.dev/doc/tutorial/workspaces
https://go.dev/blog/get-familiar-with-workspaces
https://zenn.dev/kimuson13/articles/go-workspace-mode-
https://future-architect.github.io/articles/20220216a/

これでこの章は終わりです。
適宜コンテナは削除しておいてください!
次章ではドメイン駆動設計を用いてアプリケーションを「設計」します。

現段階のディレクトリ

.
├── .github
│   └── workflows
│       └── test.yml
├── .gitignore
├── Dockerfile.app
├── Makefile
├── app
│   ├── .air.toml
│   ├── go.mod
│   ├── go.sum
│   ├── infrastructure
│   │   └── db
│   │       └── mysql
│   │           └── conf.d
│   │               └── my.conf
│   ├── main.go
│   └── tmp
│       ├── build-errors.log
│       └── main
├── compose.yml
├── go.work
└── pkg
    └── go.mod