go-clean-archを使ってGoでClean Architectureを実現する
Daniel Hayes
Full-Stack Engineer · Leapcell

あなたのGoプロジェクトのコードアーキテクチャは何ですか?ヘキサゴナルアーキテクチャですか?オニオンアーキテクチャですか?あるいはDDDでしょうか?プロジェクトがどのアークテクチャを採用するにしても、中心的な目標は常に同じであるべきです。それは、コードを理解しやすく、テストしやすく、保守しやすくすることです。
この記事では、Uncle BobのClean Architectureから始め、その中心的なアイデアを簡単に分析し、go-clean-archリポジトリと組み合わせて、これらのアーキテクチャの概念をGoプロジェクトでどのように実装するかを深く掘り下げます。
Clean Architecture
Clean Architectureは、Uncle Bobによって提唱されたソフトウェアアーキテクチャ設計の哲学です。その目標は、階層構造と明確な依存関係ルールを通じて、ソフトウェアシステムを理解しやすく、テストしやすく、保守しやすくすることです。その中心的なアイデアは、関心の分離であり、システムの中核となるビジネスロジック(ユースケース)が実装の詳細(フレームワーク、データベースなど)に依存しないようにすることです。
Clean Architectureの中心的なアイデアは独立性です。
- フレームワークからの独立性: 特定のフレームワーク(Gin、GRPCなど)に依存しません。フレームワークは、アーキテクチャの中核ではなく、ツールとして扱う必要があります。
- UIからの独立性: ユーザーインターフェースは、システムの他の部分に影響を与えることなく簡単に変更できます。たとえば、Web UIは、ビジネスルールを変更せずにコンソールUIに置き換えることができます。
- データベースからの独立性: データベースは、コアビジネスロジックに影響を与えることなく切り替えることができます(例えば、MySQLからMongoDBへ)。
- 外部ツールからの独立性: 外部依存関係(サードパーティライブラリなど)は、システムの中核に直接影響を与えないように分離する必要があります。
構造図
図に示すように、Clean Architectureは一連の同心円として記述されており、各レイヤーは異なるシステムの責任を表しています。
-
コアエンティティ
- 場所: 最内層
- 責任: システムのビジネスルールを定義します。エンティティはアプリケーションの中核となるオブジェクトであり、独立したライフサイクルを持ちます。
- 独立性: ビジネスルールから完全に独立しており、ビジネスルールが変更された場合にのみ変更されます。
-
ユースケース/ サービス
- 場所: エンティティのすぐ隣のレイヤー
- 責任: アプリケーションのビジネスロジックを実装します。システムのさまざまな操作(ユースケース)のフローを定義し、ユーザーの要件が満たされるようにします。
- 役割: ユースケースレイヤーはエンティティレイヤーを呼び出し、データフローを調整し、応答を決定します。
-
インターフェースアダプター
- 場所: 次の外側のレイヤー
- 責任: 外部システム(UI、データベースなど)からのデータを、内部レイヤーが理解できる形式に変換し、コアビジネスロジックを外部システムで使用できる形式に変換する役割も担います。
- 例: HTTPリクエストデータを内部モデル(クラスや構造体など)に変換したり、ユースケースの出力データをユーザーに提示したりします。
- コンポーネント: コントローラー、ゲートウェイ、プレゼンターなどが含まれます。
-
フレームワークとドライバー
- 場所: 最外層
- 責任: データベース、UI、メッセージキューなど、外部世界とのインタラクションを実装します。
- 特徴: このレイヤーは内側のレイヤーに依存しますが、その逆はありません。これは、システムの中で最も交換しやすい部分です。
go-clean-arch プロジェクト
go-clean-archは、Clean Architectureを実装するサンプルGoプロジェクトです。このプロジェクトは、4つのドメインレイヤーに分かれています。
モデルレイヤー
目的: ドメインのコアデータ構造を定義し、プロジェクト内のビジネスエンティティ、例えばArticleやAuthorを記述します。
対応する理論レイヤー: エンティティレイヤー。
例:
package domain import ( "time" ) type Article struct { ID int64 `json:"id"` Title string `json:"title" validate:"required"` Content string `json:"content" validate:"required"` Author Author `json:"author"` UpdatedAt time.Time `json:"updated_at"` CreatedAt time.Time `json:"created_at"` }
リポジトリレイヤー
目的: データソース(データベースやキャッシュなど)とのインタラクションを担当し、ユースケースレイヤーがデータにアクセスするための統一されたインターフェースを提供します。
対応する理論レイヤー: フレームワークとドライバー。
例:
package mysql import ( "context" "database/sql" "fmt" "github.com/sirupsen/logrus" "github.com/bxcodec/go-clean-arch/domain" "github.com/bxcodec/go-clean-arch/internal/repository" ) type ArticleRepository struct { Conn *sql.DB } // NewArticleRepository will create an object that represents the article.Repository interface func NewArticleRepository(conn *sql.DB) *ArticleRepository { return &ArticleRepository{conn} } func (m *ArticleRepository) fetch(ctx context.Context, query string, args ...interface{}) (result []domain.Article, err error) { rows, err := m.Conn.QueryContext(ctx, query, args...) if err != nil { logrus.Error(err) return nil, err } defer func() { errRow := rows.Close() if errRow != nil { logrus.Error(errRow) } }() result = make([]domain.Article, 0) for rows.Next() { t := domain.Article{} authorID := int64(0) err = rows.Scan( &t.ID, &t.Title, &t.Content, &authorID, &t.UpdatedAt, &t.CreatedAt, ) if err != nil { logrus.Error(err) return nil, err } t.Author = domain.Author{ ID: authorID, } result = append(result, t) } return result, nil } func (m *ArticleRepository) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { query := `SELECT id,title,content, author_id, updated_at, created_at FROM article WHERE ID = ?` list, err := m.fetch(ctx, query, id) if err != nil { return domain.Article{}, err } if len(list) > 0 { res = list[0] } else { return res, domain.ErrNotFound } return }
ユースケース/サービスレイヤー
目的: システムのコアアプリケーションロジックを定義し、ドメインモデルと外部インタラクションの間のブリッジとして機能します。
対応する理論レイヤー: ユースケース/ サービス。
例:
package article import ( "context" "time" "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" "github.com/bxcodec/go-clean-arch/domain" ) type ArticleRepository interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } type AuthorRepository interface { GetByID(ctx context.Context, id int64) (domain.Author, error) } type Service struct { articleRepo ArticleRepository authorRepo AuthorRepository } func NewService(a ArticleRepository, ar AuthorRepository) *Service { return &Service{ articleRepo: a, authorRepo: ar, } } func (a *Service) GetByID(ctx context.Context, id int64) (res domain.Article, err error) { res, err = a.articleRepo.GetByID(ctx, id) if err != nil { return } resAuthor, err := a.authorRepo.GetByID(ctx, res.Author.ID) if err != nil { return domain.Article{}, err } res.Author = resAuthor return }
デリバリーレイヤー
目的: 外部リクエストを受け取り、ユースケースレイヤーを呼び出し、結果を外部(HTTPクライアントやCLIユーザーなど)に返す役割を担います。
対応する理論レイヤー: インターフェースアダプター。
例:
package rest import ( "context" "net/http" "strconv" "github.com/bxcodec/go-clean-arch/domain" ) type ResponseError struct { Message string `json:"message"` } type ArticleService interface { GetByID(ctx context.Context, id int64) (domain.Article, error) } // ArticleHandler represents the HTTP handler for articles type ArticleHandler struct { Service ArticleService } func NewArticleHandler(e *echo.Echo, svc ArticleService) { handler := &ArticleHandler{ Service: svc, } e.GET("/articles/:id", handler.GetByID) } func (a *ArticleHandler) GetByID(c echo.Context) error { idP, err := strconv.Atoi(c.Param("id")) if err != nil { return c.JSON(http.StatusNotFound, domain.ErrNotFound.Error()) } id := int64(idP) ctx := c.Request().Context() art, err := a.Service.GetByID(ctx, id) if err != nil { return c.JSON(getStatusCode(err), ResponseError{Message: err.Error()}) } return c.JSON(http.StatusOK, art) }
go-clean-arch プロジェクトの基本的なコードアーキテクチャは次のとおりです。
go-clean-arch/
├── internal/
│ ├── rest/
│ │ └── article.go # デリバリーレイヤー
│ ├── repository/
│ │ ├── mysql/
│ │ │ └── article.go # リポジトリレイヤー
├── article/
│ └── service.go # ユースケース/サービスレイヤー
├── domain/
│ └── article.go # モデルレイヤー
go-clean-archプロジェクトでは、各レイヤー間の依存関係は次のとおりです。
- ユースケース/サービスレイヤーは、リポジトリインターフェースに依存しますが、インターフェースの実装の詳細については認識しません。
- リポジトリレイヤーはインターフェースを実装しますが、ドメインレイヤーのエンティティに依存する外部コンポーネントです。
- デリバリーレイヤー(RESTハンドラーなど)は、ユースケース/サービスレイヤーを呼び出し、外部リクエストをビジネスロジックの呼び出しに変換する役割を担います。
この設計は依存性逆転の原則に従い、コアビジネスロジックが外部実装の詳細から独立していることを保証し、その結果、テスト容易性と柔軟性が向上します。
まとめ
この記事では、Uncle BobのClean Architectureとgo-clean-archサンプルプロジェクトを組み合わせることで、GoプロジェクトでClean Architectureを実装する方法を紹介しました。システムをコアエンティティ、ユースケース、インターフェースアダプター、外部フレームワークなどのレイヤーに分割することで、関心が明確に分離され、コアビジネスロジック(ユースケース)はフレームワークやデータベースなどの外部実装の詳細から分離されます。
go-clean-archプロジェクトのアーキテクチャは、コードをレイヤー化された方法で編成し、各レイヤーの責任を明確にしています。
- モデルレイヤー(ドメインレイヤー): コアビジネスエンティティを定義し、外部実装から独立しています。
- ユースケースレイヤー: アプリケーションロジックを実装し、エンティティと外部インタラクションを調整します。
- リポジトリレイヤー: データストレージの特定の実装の詳細を実装します。
- デリバリーレイヤー: 外部リクエストを処理し、結果を返します。
これは単なるサンプルプロジェクトです。実際のプロジェクトのアーキテクチャ設計は、実際的な要件、チームの開発習慣、および標準に従って柔軟に調整する必要があります。中心的な目標は、レイヤー化の原則を維持し、コードが理解しやすく、テストしやすく、保守しやすく、システムの長期的なスケーラビリティと進化をサポートすることです。
Goプロジェクトのホスティングには、Leapcellが最適です。
Leapcellは、Webホスティング、非同期タスク、Redisのための次世代サーバーレスプラットフォームです。
多言語サポート
- Node.js、Python、Go、またはRustで開発できます。
無制限のプロジェクトを無料でデプロイ
- 使用量に応じてのみ料金が発生します。リクエストも請求もありません。
比類のないコスト効率
- アイドル料金なしの従量課金制。
- 例:25ドルで、平均応答時間60msで694万リクエストをサポートします。
合理化された開発者エクスペリエンス
- 簡単なセットアップのための直感的なUI。
- 完全に自動化されたCI/CDパイプラインとGitOps統合。
- 実用的なインサイトを得るためのリアルタイムメトリクスとログ。
簡単なスケーラビリティと高性能
- 高い同時実行性を容易に処理するための自動スケーリング。
- 運用オーバーヘッドゼロ。構築に集中するだけです。
ドキュメントで詳細をご覧ください!
Xでフォローしてください:@LeapcellHQ