Go言語の構造体(struct)の型定義の整理術
はじめに
Go言語での開発において、struct(構造体)はデータ構造を定義するための基本的な機能です。しかし、プロジェクトが大規模になるにつれて、どこで、どのようにstructを定義すべきかという問題に直面します。特に、複数のパッケージをまたいで同じようなデータ構造を扱いたい場合に、この問題は顕著になります。
本記事では、Go言語のstructの型定義を整理し、見通しの良いコードを維持するための実践的なアプローチについて解説します。
課題:structの定義場所が分からない
例えば、userという概念を考えます。ユーザー情報は、APIリクエストの入力(controller)、データベースへの保存(model or repository)、外部サービスへの出力(client)など、様々なレイヤーで利用されます。
このとき、以下のような疑問が浮かびます。
UserControllerで使うUserstructと、UserRepositoryで使うUserstructは同じものであるべきか?- もし同じでないなら、どのように変換(マッピング)すべきか?
- 共通の
structを定義する場合、どのパッケージに置くべきか?
これらの問いに明確なルールなしで立ち向かうと、コードの依存関係が複雑になり、変更が困難なシステムが出来上がってしまいます。
解決策:レイヤーごとにstructを定義する
結論から言うと、各レイヤー(controller, usecase, repositoryなど)の責務に応じて、それぞれ独自のstructを定義するのが最も堅牢なアプローチです。
これにより、各レイヤーが自身の責務に集中でき、他のレイヤーの変更から影響を受けにくくなります。
// controller/user_controller.go
// APIリクエストの形式を定義
package controller
type UserCreateRequest struct {
Name string `json:"name"`
Email string `json:"email"`
}
// entity/user.go
// アプリケーションの核となるドメインオブジェクトを定義
package entity
type User struct {
ID string
Name string
Email string
}
// repository/user_repository.go
// データベースのスキーマ(今回はDynamoDBの例)を定義
package repository
type UserDataModel struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
Name string `dynamodbav:"Name"`
Email string `dynamodbav:"Email"`
CreatedAt int64 `dynamodbav:"CreatedAt"`
}
なぜこのアプローチが良いのか
関心の分離 (Separation of Concerns)
controllerのstructは、APIのインターフェース(JSONの形式、バリデーションルールなど)に責任を持ちます。repositoryのstructは、データベースのスキーマ(テーブル設計、カラム名、データ型など)に責任を持ちます。entityのstructは、アプリケーションのコアなビジネスロジック(ドメイン)そのものを表現し、特定の技術(APIやDB)には依存しません。
このように責務を分離することで、例えば「APIのレスポンス形式だけを変更したい」といった場合に、
repositoryやentityのコードに影響を与えることなく修正が可能になります。依存関係の明確化
- 依存の方向を
controller→usecase→repositoryのように一方向に保つことができます。 repositoryがcontrollerのstructに依存する、といった循環参照や意図しない依存関係を防ぎます。
- 依存の方向を
柔軟性と拡張性
- 将来的にデータベースをMySQLからDynamoDBへ移行する場合でも、変更は
repositoryレイヤーとそのstructに限定されます。 - gRPCインターフェースを追加する場合も、
controllerレイヤーに新しいstructを追加するだけで対応できます。
- 将来的にデータベースをMySQLからDynamoDBへ移行する場合でも、変更は
型変換のオーバーヘッドは
レイヤーごとにstructを定義すると、それらの間でデータを変換するコードが必要になります。これは一見、手間に思えます。
// usecase/user_usecase.go
package usecase
// Usecaseはcontrollerの型とrepositoryの型に依存する
import (
"my-project/controller"
"my-project/entity"
"my-project/repository"
"time"
)
// UserRepository defines the interface for user persistence.
type UserRepository interface {
Save(user *repository.UserDataModel) error
}
// UserUsecase handles the business logic for users.
type UserUsecase struct {
userRepository UserRepository
}
// generateID is a placeholder for ID generation logic.
func generateID() string {
return "new-id"
}
func (u *UserUsecase) CreateUser(req *controller.UserCreateRequest) (*entity.User, error) {
// 1. controllerのstructからentityのstructへ変換
user := &entity.User{
ID: generateID(), // IDを生成
Name: req.Name,
Email: req.Email,
}
// 2. entityをrepositoryのデータモデルに変換
dataModel := &repository.UserDataModel{
PK: "USER#" + user.ID,
SK: "METADATA",
Name: user.Name,
Email: user.Email,
CreatedAt: time.Now().Unix(),
}
// 3. データベースへ保存
if err := u.userRepository.Save(dataModel); err != nil {
return nil, err
}
return user, nil
}
確かに、この変換処理は一見すると冗長に思えます。しかし、この「冗長性」こそが、各レイヤーの独立性を保ち、長期的なメンテナンス性を高めるための「トレードオフ」なのです。
小規模なアプリケーションでは過剰に感じるかもしれませんが、チームでの開発や長期的な運用を視野に入れるなら、この設計は非常に有効です。
まとめ
Go言語におけるstructの型定義は、アプリケーションの設計思想を反映する重要な要素です。レイヤーごとにstructを分離・定義することで、関心の分離を実現し、変更に強く、スケールしやすいアプリケーションを構築できます。
最初は少し手間がかかるように感じるかもしれませんが、このプラクティスを導入することで、将来の自分やチームメイトが助かる場面が必ず訪れるでしょう。


