メインコンテンツにスキップ

Go言語の構造体(struct)の型定義の整理術

はじめに

Go言語での開発において、struct(構造体)はデータ構造を定義するための基本的な機能です。しかし、プロジェクトが大規模になるにつれて、どこで、どのようにstructを定義すべきかという問題に直面します。特に、複数のパッケージをまたいで同じようなデータ構造を扱いたい場合に、この問題は顕著になります。

本記事では、Go言語のstructの型定義を整理し、見通しの良いコードを維持するための実践的なアプローチについて解説します。

課題:structの定義場所が分からない

例えば、userという概念を考えます。ユーザー情報は、APIリクエストの入力(controller)、データベースへの保存(model or repository)、外部サービスへの出力(client)など、様々なレイヤーで利用されます。

このとき、以下のような疑問が浮かびます。

  • UserControllerで使うUser structと、UserRepositoryで使うUser structは同じものであるべきか?
  • もし同じでないなら、どのように変換(マッピング)すべきか?
  • 共通の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"`
}

なぜこのアプローチが良いのか

  1. 関心の分離 (Separation of Concerns)

    • controllerstructは、APIのインターフェース(JSONの形式、バリデーションルールなど)に責任を持ちます。
    • repositorystructは、データベースのスキーマ(テーブル設計、カラム名、データ型など)に責任を持ちます。
    • entitystructは、アプリケーションのコアなビジネスロジック(ドメイン)そのものを表現し、特定の技術(APIやDB)には依存しません。

    このように責務を分離することで、例えば「APIのレスポンス形式だけを変更したい」といった場合に、repositoryentityのコードに影響を与えることなく修正が可能になります。

  2. 依存関係の明確化

    • 依存の方向をcontrollerusecaserepositoryのように一方向に保つことができます。
    • repositorycontrollerstructに依存する、といった循環参照や意図しない依存関係を防ぎます。
  3. 柔軟性と拡張性

    • 将来的にデータベースをMySQLからDynamoDBへ移行する場合でも、変更はrepositoryレイヤーとそのstructに限定されます。
    • gRPCインターフェースを追加する場合も、controllerレイヤーに新しいstructを追加するだけで対応できます。

型変換のオーバーヘッドは

レイヤーごとに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を分離・定義することで、関心の分離を実現し、変更に強く、スケールしやすいアプリケーションを構築できます。

最初は少し手間がかかるように感じるかもしれませんが、このプラクティスを導入することで、将来の自分やチームメイトが助かる場面が必ず訪れるでしょう。