0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで始めるクリーンアーキテクチャ入門 ─ 最小構成で理解する実務実装ガイド

Posted at

はじめに

Go は軽量で高速、並行処理に強い言語ですが、
設計をどのように組むか が実務では非常に重要です。

  • Handler が肥大化する
  • Repository がロジックを持ち始める
  • 依存関係がぐちゃぐちゃになる
  • 「クリーンアーキテクチャって難しくない?」となる

Go学習者・実務1〜3年目の若手から特によく聞く悩みです。

そこで本記事では、

最小構成で"理解しやすく実務で使いやすい"クリーンアーキテクチャ

を、
図・コード・考え方・つまづきポイント までまとめて解説します。

初心者〜中級者どちらにも刺さる内容になっています。


🎯 結論:Goでは「外→内への依存を守るだけ」で十分強い

クリーンアーキテクチャを完全実装しようとすると難しいですが、
Goではシンプルな4層構造にすると最も扱いやすいです。

/domain          ← ビジネスルール(最内側)
/usecase         ← ユースケース(アプリケーションロジック)
/infrastructure  ← DB / 外部API
/interface       ← HTTPハンドラー(最外側)

依存関係は次の方向だけにします👇

  • ✔ 内側は外側を知らない(domain は永遠に純粋)
  • ✔ 外側が内側に依存する(依存の流れが一方向)
  • ✔ Go特有の"interfaceの軽さ"が相性抜群

🧪 今回の題材:ユーザー取得 API

GET /users/{id} を例に、4層のコードをすべて書いていきます。


🟦 domain層 ─ ビジネスルール(最内側)

  • 依存してよいのは Go標準ライブラリだけ
  • DBもHTTPも知らない
  • 「どんなエンティティが存在するか?」を定義する
package domain

type User struct {
    ID   int
    Name string
    Age  int
}

type UserRepository interface {
    FindByID(id int) (*User, error)
}

domain層は「純粋なビジネスルール」。
1年後・5年後に仕様が変わっても壊れない構造を目指します。


🟩 usecase層 ─ アプリケーションロジック

  • domain の interface を使う
  • DB実装は知らない
  • 「アプリとして何を実現したいか?」を書く場所
package usecase

import "example.com/project/domain"

type UserUsecase struct {
    Repo domain.UserRepository
}

func NewUserUsecase(r domain.UserRepository) *UserUsecase {
    return &UserUsecase{Repo: r}
}

func (u *UserUsecase) GetUser(id int) (*domain.User, error) {
    user, err := u.Repo.FindByID(id)
    if err != nil {
        return nil, err
    }

    // 必要ならここに業務ルールを追加
    return user, nil
}

ここに SQL を書いたり、外部APIを呼んだりすると
usecase が infrastructure に汚染されるためNGです。


🟧 infrastructure層 ─ DBや外部APIの実装

  • domain.UserRepository を 実装するだけ
  • usecase層に依存してはいけない
  • 実務ではここが一番"変わりやすい層"
package infrastructure

import (
    "database/sql"
    "example.com/project/domain"
)

type UserRepositoryImpl struct {
    DB *sql.DB
}

func NewUserRepository(db *sql.DB) *UserRepositoryImpl {
    return &UserRepositoryImpl{DB: db}
}

func (r *UserRepositoryImpl) FindByID(id int) (*domain.User, error) {
    row := r.DB.QueryRow("SELECT id, name, age FROM users WHERE id = ?", id)

    var user domain.User
    if err := row.Scan(&user.ID, &user.Name, &user.Age); err != nil {
        return nil, err
    }

    return &user, nil
}

SQLを直接 usecase や interface に書き始めると すぐスパゲッティ化します。
DBロジックは infrastructure に閉じ込めましょう。


🟥 interface層 ─ HTTPハンドラー

ルーティング・レスポンスなど、
「外部との接点」だけを担当します。

ここでは chi を使用しますが、Gin/Echo に置き換えても問題ありません。

package interfaces

import (
    "encoding/json"
    "net/http"
    "strconv"

    "github.com/go-chi/chi/v5"
    "example.com/project/usecase"
)

type UserHandler struct {
    UC *usecase.UserUsecase
}

func NewUserHandler(uc *usecase.UserUsecase) *UserHandler {
    return &UserHandler{UC: uc}
}

func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    idStr := chi.URLParam(r, "id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "invalid id", http.StatusBadRequest)
        return
    }

    user, err := h.UC.GetUser(id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

🧱 main.go ─ 全部を組み立てる

DI(依存性注入)で各層をつなぎ合わせます。

package main

import (
    "database/sql"
    "log"
    "net/http"

    "github.com/go-chi/chi/v5"
    _ "github.com/go-sql-driver/mysql"

    "example.com/project/infrastructure"
    "example.com/project/interfaces"
    "example.com/project/usecase"
)

func main() {
    db, err := sql.Open("mysql", "user:pass@tcp(localhost:3306)/sampledb")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // DI(依存性注入)
    userRepo := infrastructure.NewUserRepository(db)
    userUC := usecase.NewUserUsecase(userRepo)
    userHandler := interfaces.NewUserHandler(userUC)

    r := chi.NewRouter()
    r.Get("/users/{id}", userHandler.GetUser)

    log.Println("Server running at :8080")
    http.ListenAndServe(":8080", r)
}

🔍 実務でつまづきやすいポイント(経験談つき)

❌ つまづき1:interfaceとinfrastructureの区別が曖昧

→ Handler と DB を同じ場所に置きがち(層が崩壊)

❌ つまづき2:usecaseに外部API呼び出しを書いてしまう

→ "usecase汚染" が起きてテスト不能になる

❌ つまづき3:repositoryがビジネスロジックを持ち始める

→ domain層が薄くなり保守困難へ

✔ 対策

  • 「変わりやすいもの(DB・フレームワーク)は外側」
  • 「変わりにくいもの(ビジネスルール)は内側」
  • usecaseは"ユースケース実現だけ"に徹底する

🧩 まとめ:Goは"最小限のクリーンアーキテクチャ"が最も強い

  • domain:不変のビジネスルール
  • usecase:アプリの目的を達成するロジック
  • infrastructure:変わりやすい外界の詳細
  • interface:外部からの入り口
  • 依存は 外 → 内 に収束させる
  • Goはinterfaceが軽いので相性最高

シンプルなのに保守性が高い構造になります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?