はじめに
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が軽いので相性最高
シンプルなのに保守性が高い構造になります。