Go言語のインターフェースと構造体を効果的に活用すると、拡張性の高いシステム設計が可能になります。
ユースケースに沿ったサンプルコード学習の
備忘録まとめです。
基本構造:インターフェースと実装の分離
// repositories/item_repository.go
package repositories
import "gin-fleamarket/models"
// インターフェース: 商品リポジトリの契約
type IItemRepository interface {
    FindAll() ([]models.Item, error)
    FindByID(id uint) (*models.Item, error)
}
// 構造体: メモリ実装
type ItemMemoryRepository struct {
    items []models.Item
}
// コンストラクタ
func NewItemMemoryRepository(items []models.Item) IItemRepository {
    return &ItemMemoryRepository{items: items}
}
// メソッド実装
func (r *ItemMemoryRepository) FindAll() ([]models.Item, error) {
    return r.items, nil
}
func (r *ItemMemoryRepository) FindByID(id uint) (*models.Item, error) {
    for _, item := range r.items {
        if item.ID == id {
            return &item, nil
        }
    }
    return nil, errors.New("Item not found")
}
メリット1: 実装の差し替えが容易
データベース実装を追加
// repositories/item_db_repository.go
package repositories
import (
    "database/sql"
    "gin-fleamarket/models"
)
// 構造体: データベース実装
type ItemDBRepository struct {
    db *sql.DB
}
func NewItemDBRepository(db *sql.DB) IItemRepository {
    return &ItemDBRepository{db: db}
}
func (r *ItemDBRepository) FindAll() ([]models.Item, error) {
    rows, err := r.db.Query("SELECT id, name, price FROM items")
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    var items []models.Item
    for rows.Next() {
        var item models.Item
        if err := rows.Scan(&item.ID, &item.Name, &item.Price); err != nil {
            return nil, err
        }
        items = append(items, item)
    }
    return items, nil
}
func (r *ItemDBRepository) FindByID(id uint) (*models.Item, error) {
    row := r.db.QueryRow("SELECT id, name, price FROM items WHERE id = ?", id)
    var item models.Item
    if err := row.Scan(&item.ID, &item.Name, &item.Price); err != nil {
        return nil, err
    }
    return &item, nil
}
メリット2: サービスの実装がインターフェースに依存
// services/item_service.go
package services
import (
    "gin-fleamarket/models"
    "gin-fleamarket/repositories"
)
type ItemService struct {
    repo repositories.IItemRepository // インターフェースに依存
}
func NewItemService(repo repositories.IItemRepository) *ItemService {
    return &ItemService{repo: repo}
}
func (s *ItemService) GetAllItems() ([]models.Item, error) {
    return s.repo.FindAll()
}
func (s *ItemService) GetItemByID(id uint) (*models.Item, error) {
    return s.repo.FindByID(id)
}
メリット3: 実行時に実装を選択可能
// main.go
package main
import (
    "database/sql"
    "fmt"
    "gin-fleamarket/repositories"
    "gin-fleamarket/services"
    "log"
    
    _ "github.com/go-sql-driver/mysql"
)
func main() {
    // 環境に応じて実装を切り替え
    var repo repositories.IItemRepository
    
    if config.UseMemoryDB {
        // メモリ実装
        initialItems := []models.Item{
            {ID: 1, Name: "Item A", Price: 1000},
            {ID: 2, Name: "Item B", Price: 2000},
        }
        repo = repositories.NewItemMemoryRepository(initialItems)
    } else {
        // データベース実装
        db, err := sql.Open("mysql", "user:password@/dbname")
        if err != nil {
            log.Fatal(err)
        }
        defer db.Close()
        repo = repositories.NewItemDBRepository(db)
    }
    
    // サービスの初期化(実装の詳細を知らない)
    itemService := services.NewItemService(repo)
    
    // 同じインターフェースで操作
    items, err := itemService.GetAllItems()
    if err != nil {
        log.Fatal(err)
    }
    
    fmt.Println("Items:", items)
}
メリット4: テストの容易性
// services/item_service_test.go
package services_test
import (
    "testing"
    "gin-fleamarket/models"
    "gin-fleamarket/repositories"
    "gin-fleamarket/services"
)
// モックリポジトリ実装
type MockItemRepository struct {
    items []models.Item
}
func (m *MockItemRepository) FindAll() ([]models.Item, error) {
    return m.items, nil
}
func (m *MockItemRepository) FindByID(id uint) (*models.Item, error) {
    for _, item := range m.items {
        if item.ID == id {
            return &item, nil
        }
    }
    return nil, errors.New("not found")
}
func TestItemService_GetAllItems(t *testing.T) {
    // モックデータ準備
    mockItems := []models.Item{
        {ID: 1, Name: "Test Item 1", Price: 100},
        {ID: 2, Name: "Test Item 2", Price: 200},
    }
    
    // モックリポジトリ作成
    mockRepo := &MockItemRepository{items: mockItems}
    
    // サービス初期化
    service := services.NewItemService(mockRepo)
    
    // テスト実行
    items, err := service.GetAllItems()
    if err != nil {
        t.Fatalf("Unexpected error: %v", err)
    }
    
    if len(items) != 2 {
        t.Errorf("Expected 2 items, got %d", len(items))
    }
}
実践的メリットまとめ
- 
拡張性の向上 - 新しいデータソース(ファイル、APIなど)を追加する際、既存のサービスコードを変更せずに実装可能
 type ItemAPIRepository struct{ /* API実装 */ } func (r *ItemAPIRepository) FindAll() ([]models.Item, error) { // APIからデータ取得 }
- 
テスト容易性 - 実際のデータベースに依存せず、モック実装でサービス層をテスト可能
- テスト実行速度の向上
 
- 
依存関係の逆転 サービスが具体的な実装ではなく抽象(インターフェース)に依存 
- 
コードの見通し向上 - 各コンポーネントの責務が明確化
- ビジネスロジック(サービス層)とデータアクセス層が分離
 
- 
段階的なリファクタリング - 最初にメモリ実装でプロトタイプ作成
- 後からデータベース実装に差し替え可能
- システム進化に伴う変更コスト最小化
 
効果的な適用パターン
- 
ストレージ層 - データベース/メモリ/ファイルストレージの切り替え
 
- 
外部サービス連携 - 実際のAPIクライアントとモック実装の切り替え
 
- 
アルゴリズム切り替え - ソートや検索アルゴリズムの差し替え
 
- 
ロギングシステム - コンソール出力/ファイル出力/クラウドロギングの切り替え
 
type Logger interface {
    Info(msg string)
    Error(msg string)
}
// 使用例
func ProcessOrder(service PaymentService, logger Logger) {
    logger.Info("処理開始")
    // ...
}