Goの型定義、全部一緒のファイルに入れてませんか?ドメインで整理する構造化設計術
はじめに:あなたのtypesパッケージは「図書館」ですか。「ゴミ箱」ですか。
Go言語でプロジェクトが成長するにつれ、structで定義された型は増え続けます。そして多くのプロジェクトで、pkg/typesのようなパッケージが作られ、そこにあらゆるドメインの型定義が雑多に放り込まれていきます。
最初は便利に思えるこの「何でも置き場」ですが、やがて関連性が不明確になり、不要な依存関係を生み出し、変更影響範囲の把握を困難にする「ゴミ箱パッケージ」と化してしまいます。
この記事では、実際のプロダクションコードで発生した「1つのファイルに無関係な型が50個以上も混在している」問題を例に、型定義を**ドメイン(関心事)**に基づいて整理し、コードの可得性・保守性・再利用性を劇的に向上させる方法を解説します。
問題のコード:あらゆるドメインの型が混在するtypes.go
以下は、認証、ユーザー、管理者、バリデーション、メトリクスなど、全く異なるドメインの型が1つのファイルに詰め込まれた典型的な悪い例です。
// ❌ 悪い例:pkg/types/types.go(関連性がカオス)
package types
// --- 認証関連? ---
type LoginRequest struct { /* ... */ }
type LoginResponse struct { /* ... */ }
// --- ユーザー関連? ---
type User struct { /* ... */ }
type UpdateUserRequest struct { /* ... */ }
// --- パスワード関連? ---
type ChangePasswordRequest struct { /* ... */ }
// --- 管理者関連? ---
type AdminUser struct { /* ... */ }
type AdminListUsersResponse struct { /* ... */ }
// --- バリデーション関連? ---
type ValidationError struct { /* ... */ }
// --- 共通レスポンス? ---
type APIResponse struct { /* ... */ }
// --- メトリクス関連? ---
type MetricsData struct { /* ... */ }
// ... さらに数十個の型が続く ...
なぜこの「ゴミ箱パッケージ」が問題なのか。
| 問題点 | 詳細 |
|---|---|
| 関連性の欠如 | LoginRequestとMetricsDataがなぜ同じファイルにあるのか、誰も説明できません。コードの意図が不明確になります。 |
| 不要な依存関係 | 認証機能を実装したいだけなのに、AdminUserやMetricsDataなど、全く無関係な型まで一緒にインポートされてしまいます。 |
| 変更影響の恐怖 | ValidationError型を少し変更しただけで、このファイルに依存する全ての機能(認証、ユーザー管理、管理者機能など)に影響がないか確認する必要があり、変更コストが非常に高くなります。 |
| 命名の衝突 | UserとAdminUserのように、似て非なる型が同じファイルに存在し、どちらが何を指すのか混乱を招きます。 |
解決策:ドメイン(関心事)に基づき、型を分類・分割する
このカオスを解決する鍵は、**「何をするための型か。」**というドメイン(関心事)に基づいてファイルを分割することです。
改善後のパッケージ構造
// ✅ 改善後のパッケージ構造
pkg/
└── types/
├── admin.go # 管理者機能に関する型
├── auth.go # 認証(サインアップ、ログイン)に関する型
├── common.go # ページネーションなど、複数のドメインで使われる共通の型
├── user.go # 一般ユーザーやプロフィールに関する型
└── validation.go # バリデーションエラーに関する型
Step 1: 認証関連の型をauth.goに分離する
認証フロー(ログイン、サインアップ、トークンリフレッシュなど)に特化した型をまとめます。
// ✅ pkg/types/auth.go
package types
// ログイン要求
type LoginRequest struct { /* ... */ }
// ログイン応答
type LoginResponse struct { /* ... */ }
// サインアップ要求
type SignUpRequest struct { /* ... */ }
Step 2: ユーザー関連の型をuser.goに分離する
ユーザーの基本情報、プロフィール、関連操作の型をまとめます。パスワード変更などもユーザーに密接に関連するため、ここに含めます。
// ✅ pkg/types/user.go
package types
// ユーザー基本情報
type User struct { /* ... */ }
// ユーザープロフィール(公開情報)
type UserProfile struct { /* ... */ }
// ユーザー情報更新要求
type UpdateUserRequest struct { /* ... */ }
// パスワード変更要求
type ChangePasswordRequest struct { /* ... */ }
Step 3: 管理者関連の型をadmin.goに分離する
管理者権限でのみ使用される型(管理者ユーザー情報、ユーザー作成要求など)をまとめます。
// ✅ pkg/types/admin.go
package types
// 管理者ユーザー情報
type AdminUser struct { /* ... */ }
// 管理者によるユーザー作成要求
type AdminCreateUserRequest struct { /* ... */ }
Step 4: 共通の型をcommon.goとvalidation.goに分離する
common.go: APIの基本レスポンス形式やページネーションなど、複数のドメインで横断的に利用される型を配置validation.go: バリデーションエラーの構造体など、エラーハンドリングに特化した型を配置
// ✅ pkg/types/common.go
package types
// APIレスポンス共通形式
type APIResponse struct { /* ... */ }
// ページネーション要求
type PaginationRequest struct { /* ... */ }
// ✅ pkg/types/validation.go
package types
// バリデーションエラー情報
type ValidationError struct { /* ... */ }
// 複数のバリデーションエラー
type ValidationErrors []ValidationError
Step 5: インポートを改善し、依存関係を明確化する
型を分割したことで、各機能は本当に必要な型だけをインポートできるようになります。
// ✅ 改善後のインポート
// 認証ハンドラーの実装
package auth_handler
import (
"your-project/pkg/types/auth" // 認証関連の型だけ
"your-project/pkg/types/common" // 共通レスポンスの型だけ
"your-project/pkg/types/validation" // バリデーションの型だけ
)
func HandleLogin(req *auth.LoginRequest) *common.APIResponse {
// ユーザー管理や管理者機能の型はインポート不要!
}
改善によるメリット
| メリット | 詳細 |
|---|---|
| 高い可読性 | パッケージ構造を見るだけで、どこにどのような型が定義されているか一目瞭然になります |
| 最小の依存関係 | 各機能は本当に必要な型だけをインポートするため、コンパイル時間の短縮や意図しない依存の排除につながります |
| 変更の局所化 | 認証仕様の変更はauth.goに、管理者機能の変更はadmin.goに影響が限定され、安心してコードを修正できます |
| 高い再利用性 | 「認証」や「ページネーション」といった機能を、他のプロジェクトで再利用しやすくなります |
型を整理するためのベストプラクティス
- ドメインで分類する: 「技術的な種類(構造体、インターフェース)」ではなく、「ビジネス上の関心事(認証、注文、決済)」でファイルを分けましょう。
- 依存関係は一方向を意識する:
admin→user→auth→commonのように、より具体的・高レベルな機能から、より汎用的・低レベルな機能へ依存する流れを意識すると、循環参照などの問題を避けられます。 - 命名規則を統一する:
LoginRequest/LoginResponseのように、要求と応答でペアになる命名を心がけると、関連性がより明確になります。 commonは慎重に: 何でもcommonに入れると、第二の「ゴミ箱パッケージ」が生まれます。本当に複数のドメインで横断的に使われる型だけを配置しましょう。
まとめ:構造化された型定義で、保守性の高いコードへ
型定義の整理は、後回しにされがちな地味な作業ですが、プロジェクトの長期的な健全性を左右する重要な設計活動です。
型整理のチェックリスト
- 1つのファイルに、関連性のないドメインの型が混在していないか。
- インポート時に、今作っている機能に無関係な型まで読み込まれていないか。
- 型のファイル名やパッケージ名が、その役割を明確に表しているか。
- 循環インポートが発生していないか。
「関連する型は近くに、関連しない型は遠くに」。このシンプルな原則を意識して、見通しが良く、変更に強いコードベースを構築していきましょう。


