GoにおけるClean Architectureの依存性注入パターン
はじめに
GoでClean Architectureを実践する際、他の言語(Java, C#など)とは異なるGo独自の慣習を活かした設計が求められます。本記事では、Go中級者を対象に以下の2つの重要パターンを解説します。
- インターフェース定義とコンストラクタ注入 — 消費者側でインターフェースを定義し、コンストラクタで依存を注入する
-
コンパイル時インターフェース検証 —
var _ Interface = (*Struct)(nil)による実装保証
これらを組み合わせることで、テスタブルで疎結合なアーキテクチャをGoらしく構築できます。
ディレクトリ構成
まず、本記事で扱うサンプルプロジェクトのレイヤー構成を示します。
myapp/
├── cmd/
│ └── server/
│ └── main.go # エントリーポイント(依存の組み立て)
├── domain/
│ └── model/
│ └── user.go # ドメインモデル
├── usecase/
│ ├── auth_usecase.go # ユースケース実装
│ └── repository.go # リポジトリインターフェース(usecase層が定義)
├── adapter/
│ └── mysql/
│ └── user_mysql.go # リポジトリの具象実装(MySQL)
└── handler/
└── auth_handler.go # HTTPハンドラー(AuthUsecaseインターフェースを定義)
各レイヤーの依存方向は以下の通りです。
handler → usecase → domain
↑
adapter(usecaseのインターフェースを実装)
handlerはusecaseに依存しますが、具象型ではなくインターフェースに依存します。adapterはusecaseが定義したインターフェースを実装しますが、usecaseはadapterの存在を知りません。この**依存関係の逆転(Dependency Inversion Principle)**がClean Architectureの核心です。
パターン1: インターフェース定義とコンストラクタ注入
Goの慣習 — 「消費者がインターフェースを定義する」
Javaなどでは、インターフェースは実装側(プロバイダー)が定義するのが一般的です。しかし、Goのコミュニティでは消費者(コンシューマー)がインターフェースを定義するのが慣習です。
"Accept interfaces, return structs"(インターフェースを受け取り、構造体を返す)
この原則に従うと、以下のような設計になります。
ハンドラー層がUsecaseインターフェースを定義する
// handler/auth_handler.go
package handler
import "context"
// AuthUsecase は認証操作のユースケースを定義します。
// Goの慣例に従い、インターフェースはプロバイダー(usecase)ではなく
// コンシューマー(handler)が定義します。
type AuthUsecase interface {
Signup(ctx context.Context, email, password string) error
Login(ctx context.Context, email, password string) (string, error)
}
// AuthHandler は認証操作のHTTPリクエストを処理します。
// AuthUsecaseインターフェースに依存し、JSONリクエスト/レスポンスを処理します。
type AuthHandler struct {
auth AuthUsecase
}
// NewAuthHandler はAuthHandlerの新しいインスタンスを生成します。
// 依存性注入用のコンストラクタで、外部からAuthUsecaseを注入します。
func NewAuthHandler(auth AuthUsecase) *AuthHandler {
return &AuthHandler{auth: auth}
}
このコードにはいくつかの重要なポイントがあります。
1. インターフェースの定義場所
AuthUsecaseインターフェースはhandlerパッケージ内で定義されています。usecaseパッケージではありません。handlerは「自分が必要とする振る舞い」だけをインターフェースとして宣言し、usecaseの全メソッドを知る必要はありません。
2. 構造体のフィールドは非公開
authフィールドは小文字始まりで非公開です。外部パッケージから直接アクセスできないため、必ずコンストラクタ経由で依存を注入する設計を強制できます。
3. コンストラクタが具象型を返す
NewAuthHandlerは*AuthHandler(具象型のポインタ)を返します。インターフェースではなく構造体のポインタを返すのがGoの慣例です。呼び出し側が必要に応じてインターフェースに代入すればよく、不要な抽象化を避けられます。
ユースケース層の実装
ユースケース層も同様に、自身が依存するリポジトリのインターフェースを定義します。
// usecase/repository.go
package usecase
import (
"context"
"myapp/domain/model"
)
// UserRepository はユーザーの永続化操作を定義します。
// usecase層が「自分が必要とする操作」をインターフェースとして宣言します。
type UserRepository interface {
FindByEmail(ctx context.Context, email string) (*model.User, error)
Create(ctx context.Context, user *model.User) error
}
// usecase/auth_usecase.go
package usecase
import (
"context"
"errors"
"myapp/domain/model"
"golang.org/x/crypto/bcrypt"
)
// authUsecase はAuthUsecaseの具象実装です。
// 型名を非公開にすることで、外部パッケージからはコンストラクタ経由でのみ取得可能です。
type authUsecase struct {
repo UserRepository
}
// NewAuthUsecase はauthUsecaseの新しいインスタンスを生成します。
// UserRepositoryを注入し、具象構造体のポインタを返します。
func NewAuthUsecase(repo UserRepository) *authUsecase {
return &authUsecase{repo: repo}
}
func (u *authUsecase) Signup(ctx context.Context, email, password string) error {
existing, err := u.repo.FindByEmail(ctx, email)
if err != nil {
return err
}
if existing != nil {
return errors.New("user already exists")
}
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
return u.repo.Create(ctx, &model.User{
Email: email,
Password: string(hashed),
})
}
func (u *authUsecase) Login(ctx context.Context, email, password string) (string, error) {
user, err := u.repo.FindByEmail(ctx, email)
if err != nil {
return "", err
}
if user == nil {
return "", errors.New("invalid credentials")
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return "", errors.New("invalid credentials")
}
// JWT生成処理(省略)
token := "generated-jwt-token"
return token, nil
}
ここで注目すべきは、authUsecaseがhandler.AuthUsecaseインターフェースを暗黙的に満たしている点です。Goではimplementsキーワードは不要で、メソッドシグネチャが一致すれば自動的にインターフェースを満たします。
パターン2: コンパイル時インターフェース検証
var _ Interface = (*Struct)(nil) とは何か
adapter層の実装ファイルでよく見かけるこのイディオムについて解説します。
// adapter/mysql/user_mysql.go
package mysql
import (
"context"
"database/sql"
"myapp/domain/model"
"myapp/usecase"
)
// コンパイル時にuserMySQLがusecase.UserRepositoryを満たすことを検証します。
var _ usecase.UserRepository = (*userMySQL)(nil)
type userMySQL struct {
db *sql.DB
}
func NewUserMySQL(db *sql.DB) *userMySQL {
return &userMySQL{db: db}
}
func (r *userMySQL) FindByEmail(ctx context.Context, email string) (*model.User, error) {
row := r.db.QueryRowContext(ctx, "SELECT id, email, password FROM users WHERE email = ?", email)
var user model.User
err := row.Scan(&user.ID, &user.Email, &user.Password)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userMySQL) Create(ctx context.Context, user *model.User) error {
_, err := r.db.ExecContext(ctx,
"INSERT INTO users (email, password) VALUES (?, ?)",
user.Email, user.Password,
)
return err
}
一行ずつ分解して理解する
var _ usecase.UserRepository = (*userMySQL)(nil)
この一行は4つの要素で構成されています。
| 要素 | 意味 |
|---|---|
var _ |
変数を宣言するが、ブランク識別子_で値を捨てる(実行時にメモリを消費しない) |
usecase.UserRepository |
変数の型をこのインターフェースとして宣言する |
= |
右辺の値を代入する |
(*userMySQL)(nil) |
*userMySQL型のnilポインタを生成する |
この代入が成立するためには、*userMySQLがusecase.UserRepositoryインターフェースのすべてのメソッドを実装している必要があります。もし実装が不足していれば、コンパイル時にエラーが発生します。
なぜこのパターンが必要なのか
Goのインターフェースは暗黙的に満たされるため、実装漏れに気づきにくいという問題があります。
// もしFindByEmailの実装を忘れた場合...
// コンパイル時にこのようなエラーが出る:
//
// cannot use (*userMySQL)(nil) (value of type *userMySQL)
// as usecase.UserRepository value in variable declaration:
// *userMySQL does not implement usecase.UserRepository
// (missing method FindByEmail)
このイディオムがなければ、エラーはmain.goで依存を組み立てるときまで検出されません。adapter層のファイル内で即座に検出できることで、以下のメリットがあります。
- エラーの発生箇所がadapterファイル自体になる — 修正箇所が明確
- ドキュメントとしても機能する — この構造体がどのインターフェースを実装する意図なのかが一目でわかる
- リファクタリングに強い — インターフェースにメソッドを追加した場合、すべての実装箇所で即座にコンパイルエラーが発生する
ポインタレシーバに注意
(*userMySQL)(nil)のように*(ポインタ)で書くことが重要です。Goでは、メソッドがポインタレシーバで定義されている場合、値型はインターフェースを満たしません。
// ポインタレシーバでメソッドを定義した場合
func (r *userMySQL) FindByEmail(...) { ... }
// これはコンパイルエラー(値型はポインタレシーバのメソッドを持たない)
var _ usecase.UserRepository = userMySQL{}
// これは正しい(ポインタ型はポインタレシーバのメソッドを持つ)
var _ usecase.UserRepository = (*userMySQL)(nil)
依存の組み立て — main.go
すべてのレイヤーをmain.goで組み立てます。依存の組み立て(Composition Root)はアプリケーションのエントリーポイントで一箇所にまとめるのがClean Architectureの原則です。
// cmd/server/main.go
package main
import (
"database/sql"
"log"
"net/http"
"myapp/adapter/mysql"
"myapp/handler"
"myapp/usecase"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// インフラ層のセットアップ
db, err := sql.Open("mysql", "user:password@tcp(localhost:3306)/myapp")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// 依存の組み立て(下位レイヤーから上位レイヤーへ)
userRepo := mysql.NewUserMySQL(db) // adapter: *mysql.userMySQL
authUC := usecase.NewAuthUsecase(userRepo) // usecase: *usecase.authUsecase
authHandler := handler.NewAuthHandler(authUC) // handler: *handler.AuthHandler
// ルーティング
http.HandleFunc("/signup", authHandler.Signup)
http.HandleFunc("/login", authHandler.Login)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
この組み立てコードで注目すべき点があります。
userRepoは*mysql.userMySQL型ですが、NewAuthUsecaseはusecase.UserRepositoryインターフェースを受け取ります。*mysql.userMySQLがそのインターフェースを満たしているため、暗黙的に変換されます。同様に、authUCは*usecase.authUsecase型ですが、handler.AuthUsecaseを満たしているためNewAuthHandlerに渡せます。
DIコンテナ(wireやdigなど)を使わず、純粋なコンストラクタ注入だけで依存を組み立てています。Goのコミュニティでは、DIコンテナよりもこの手動組み立て(Manual Wiring)が好まれる傾向にあります。コンパイル時に依存関係の不整合を検出でき、コードの流れが明示的で追いやすいためです。
テスタビリティ
この設計の最大のメリットはテスタビリティです。インターフェースに依存しているため、テスト時にモックを簡単に注入できます。
// handler/auth_handler_test.go
package handler
import (
"context"
"testing"
)
// モック実装 — テストファイル内でインターフェースを満たす構造体を定義
type mockAuthUsecase struct {
signupFunc func(ctx context.Context, email, password string) error
loginFunc func(ctx context.Context, email, password string) (string, error)
}
func (m *mockAuthUsecase) Signup(ctx context.Context, email, password string) error {
return m.signupFunc(ctx, email, password)
}
func (m *mockAuthUsecase) Login(ctx context.Context, email, password string) (string, error) {
return m.loginFunc(ctx, email, password)
}
func TestAuthHandler_Signup(t *testing.T) {
mock := &mockAuthUsecase{
signupFunc: func(ctx context.Context, email, password string) error {
// テスト用のロジック
return nil
},
}
h := NewAuthHandler(mock)
// hを使ったHTTPテスト...
_ = h
}
外部のモックライブラリを使わずとも、テストファイル内でインターフェースを満たす構造体を定義するだけでモックが作れます。これはGoのインターフェースが暗黙的に満たされる仕組みの恩恵です。
まとめ
本記事で紹介した2つのパターンを整理します。
コンストラクタ注入 — 消費者側でインターフェースを定義し、非公開フィールドにコンストラクタ経由で依存を注入する。これにより、各レイヤーが自分に必要な最小限のインターフェースだけに依存し、疎結合が実現される。
コンパイル時インターフェース検証 — var _ Interface = (*Struct)(nil) をadapter層に記述することで、インターフェースの実装漏れをコンパイル時に検出する。ドキュメントとしても機能し、リファクタリング時の安全網になる。
GoでClean Architectureを実践する際、DIコンテナのような外部ツールに頼らずとも、言語の機能だけで十分に美しい設計が可能です。インターフェースの暗黙的充足、コンストラクタ関数、そしてコンパイル時検証イディオム — これらを組み合わせることが、Goらしいクリーンな設計の基盤となります。
Discussion