Skip to content

Goアプリケーションセットアップ手順

1. 概要

この文書では、Go-DDDマーケットプレイスアプリケーションのバックエンド部分(app/backend)におけるGoアプリケーションのセットアップ手順と必要なモジュールの設定方法について説明します。このセットアップ手順に従うことで、ドメイン駆動設計(DDD)の原則に基づいたGoアプリケーションの開発環境を構築することができます。

1.1 アプリケーション構造

以下はGoアプリケーションの基本構造を表すコンポーネント図です:

uml diagram

1.2 セットアッププロセス

以下はGoアプリケーションのセットアッププロセスを表すアクティビティ図です:

uml diagram

2. セットアップ手順

2.1 前提条件

Goアプリケーションをセットアップする前に、以下のツールがインストールされていることを確認してください:

  1. Go 1.23.0以上: 最新のGo言語環境
  2. Docker: コンテナ化されたデータベースなどの依存サービスを実行するため
  3. Git: バージョン管理のため

2.2 プロジェクト構造の作成

以下のコマンドを実行して、プロジェクト構造を作成します:

mkdir -p app/backend/{cmd/{marketplace,gen},internal/{domain,application,infrastructure,interface},docs,features}

この構造は、ドメイン駆動設計(DDD)の原則に基づいており、以下のディレクトリが含まれています:

  • cmd: アプリケーションのエントリーポイント
  • marketplace: メインアプリケーション
  • gen: コード生成ツール
  • internal: 内部パッケージ
  • domain: ドメインモデル(エンティティ、値オブジェクト、ドメインサービス)
  • application: アプリケーションサービス(ユースケース)
  • infrastructure: インフラストラクチャ(リポジトリの実装、外部サービスとの連携)
  • interface: インターフェース(コントローラー、プレゼンター)
  • docs: Swaggerドキュメント
  • features: Cucumberテスト機能

2.3 Go Modulesの初期化

プロジェクトディレクトリに移動し、Go Modulesを初期化します:

cd app/backend
go mod init github.com/sklinkert/go-ddd

2.4 必要なパッケージのインストール

以下のコマンドを実行して、必要なパッケージをインストールします:

# Webフレームワーク
go get github.com/labstack/echo/v4

# ORM
go get github.com/jinzhu/gorm
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get gorm.io/driver/sqlite

# コード生成
go get gorm.io/gen
go get gorm.io/plugin/dbresolver

# UUID生成
go get github.com/google/uuid

# Swagger
go get github.com/swaggo/swag/cmd/swag
go get github.com/swaggo/echo-swagger

# テスト
go get github.com/stretchr/testify
go get github.com/cucumber/godog
go get github.com/testcontainers/testcontainers-go

2.5 データベース設定

PostgreSQLデータベースを使用するための設定を行います。docker-compose.ymlファイルを作成または編集して、以下の内容を追加します:

version: '3'
services:
  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

2.6 アプリケーション構造の実装

2.6.1 ドメインモデルの実装

ドメインモデル(エンティティ、値オブジェクト)を実装します。例えば、Product(商品)エンティティを作成します:

// internal/domain/entities/product.go
package entities

import (
    "time"
)

type Product struct {
    Id          string
    Name        string
    Description string
    Price       float64
    SellerId    string
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

2.6.2 リポジトリインターフェースの定義

ドメインリポジトリのインターフェースを定義します:

// internal/domain/repositories/product_repository.go
package repositories

import (
    "github.com/sklinkert/go-ddd/internal/domain/entities"
)

type ProductRepository interface {
    FindAll() ([]entities.Product, error)
    FindById(id string) (*entities.Product, error)
    Create(product *entities.Product) error
    Update(product *entities.Product) error
    Delete(id string) error
}

2.6.3 リポジトリの実装

リポジトリの実装を作成します:

// internal/infrastructure/db/postgres/product_repository.go
package postgres

import (
    "github.com/sklinkert/go-ddd/internal/domain/entities"
    "gorm.io/gorm"
    "time"
)

type DBProduct struct {
    Id          string `gorm:"primaryKey"`
    Name        string
    Description string
    Price       float64
    SellerId    string
    CreatedAt   time.Time
    UpdatedAt   time.Time
}

type GormProductRepository struct {
    db *gorm.DB
}

func NewGormProductRepository(db *gorm.DB) *GormProductRepository {
    return &GormProductRepository{db: db}
}

func (r *GormProductRepository) FindAll() ([]entities.Product, error) {
    var dbProducts []DBProduct
    result := r.db.Find(&dbProducts)
    if result.Error != nil {
        return nil, result.Error
    }

    products := make([]entities.Product, len(dbProducts))
    for i, dbProduct := range dbProducts {
        products[i] = toDomainProduct(dbProduct)
    }
    return products, nil
}

// 他のメソッドも同様に実装

2.6.4 アプリケーションサービスの実装

アプリケーションサービス(ユースケース)を実装します:

// internal/application/services/product_service.go
package services

import (
    "github.com/sklinkert/go-ddd/internal/domain/entities"
    "github.com/sklinkert/go-ddd/internal/domain/repositories"
    "github.com/google/uuid"
    "time"
)

type ProductService struct {
    productRepo repositories.ProductRepository
    sellerRepo  repositories.SellerRepository
}

func NewProductService(productRepo repositories.ProductRepository, sellerRepo repositories.SellerRepository) *ProductService {
    return &ProductService{
        productRepo: productRepo,
        sellerRepo:  sellerRepo,
    }
}

func (s *ProductService) GetAllProducts() ([]entities.Product, error) {
    return s.productRepo.FindAll()
}

func (s *ProductService) CreateProduct(product *entities.Product) error {
    product.Id = uuid.New().String()
    product.CreatedAt = time.Now()
    product.UpdatedAt = time.Now()
    return s.productRepo.Create(product)
}

// 他のメソッドも同様に実装

2.6.5 コントローラーの実装

RESTful APIコントローラーを実装します:

// internal/interface/api/rest/product_controller.go
package rest

import (
    "github.com/labstack/echo/v4"
    "github.com/sklinkert/go-ddd/internal/application/services"
    "github.com/sklinkert/go-ddd/internal/domain/entities"
    "net/http"
)

type ProductController struct {
    productService *services.ProductService
}

func NewProductController(e *echo.Echo, productService *services.ProductService) {
    controller := &ProductController{
        productService: productService,
    }

    e.GET("/api/v1/products", controller.GetAllProducts)
    e.POST("/api/v1/products", controller.CreateProduct)
    // 他のエンドポイントも同様に設定
}

// @Summary 全ての商品を取得
// @Description 全ての商品のリストを取得します
// @Tags products
// @Accept json
// @Produce json
// @Success 200 {array} entities.Product
// @Router /api/v1/products [get]
func (c *ProductController) GetAllProducts(ctx echo.Context) error {
    products, err := c.productService.GetAllProducts()
    if err != nil {
        return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
    }
    return ctx.JSON(http.StatusOK, products)
}

// 他のハンドラーも同様に実装

2.6.6 メインアプリケーションの実装

メインアプリケーションのエントリーポイントを実装します:

// cmd/marketplace/main.go
// @title Marketplace API
// @version 1.0
// @description This is a marketplace API server.
// @host localhost:9090
// @BasePath /api/v1
package main

import (
    _ "github.com/jinzhu/gorm/dialects/postgres"
    "github.com/labstack/echo/v4"
    _ "github.com/sklinkert/go-ddd/docs" // Swaggerドキュメントのインポート
    "github.com/sklinkert/go-ddd/internal/application/services"
    postgres2 "github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
    "github.com/sklinkert/go-ddd/internal/interface/api/rest"
    echoSwagger "github.com/swaggo/echo-swagger"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "log"
)

func main() {
    dsn := "host=localhost user=root password=password dbname=mydb port=5432 sslmode=disable TimeZone=Asia/Shanghai"
    port := ":9090"

    gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalf("Failed to connect to database: %v", err)
    }

    // データベースマイグレーション
    err = gormDB.AutoMigrate()
    if err != nil {
        log.Fatalf("Failed to migrate database: %v", err)
    }

    // リポジトリの初期化
    productRepo := postgres2.NewGormProductRepository(gormDB)
    sellerRepo := postgres2.NewGormSellerRepository(gormDB)

    // サービスの初期化
    productService := services.NewProductService(productRepo, sellerRepo)
    sellerService := services.NewSellerService(sellerRepo)

    // Echoサーバーの初期化
    e := echo.New()

    // Swagger UIのエンドポイントを設定
    e.GET("/swagger/*", echoSwagger.WrapHandler)

    // コントローラーの初期化
    rest.NewProductController(e, productService)
    rest.NewSellerController(e, sellerService)

    // サーバーの起動
    if err := e.Start(port); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}

2.7 Swaggerドキュメントの設定

Swaggerドキュメントを生成するためのツールをセットアップします:

// cmd/gen/generate_swagger.go
package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "path/filepath"
    "runtime"
)

func main() {
    // バックエンドディレクトリへのパスを取得
    currentDir, err := os.Getwd()
    if err != nil {
        log.Fatalf("Failed to get current directory: %v", err)
    }

    // スクリプトがapp/backend/cmd/genにあると仮定
    backendDir := filepath.Join(currentDir, "app", "backend")

    // すでにバックエンドディレクトリにいるかチェック
    if _, err := os.Stat(filepath.Join(currentDir, "cmd", "marketplace", "main.go")); err == nil {
        // すでにバックエンドディレクトリにいる
        backendDir = currentDir
    } else if _, err := os.Stat(filepath.Join(currentDir, "app", "backend", "cmd", "marketplace", "main.go")); err == nil {
        // プロジェクトルートにいる
        backendDir = filepath.Join(currentDir, "app", "backend")
    } else if _, err := os.Stat(filepath.Join(currentDir, "..", "..", "cmd", "marketplace", "main.go")); err == nil {
        // app/backend/cmd/genにいる
        backendDir = filepath.Join(currentDir, "..", "..")
    }

    // バックエンドディレクトリに移動
    err = os.Chdir(backendDir)
    if err != nil {
        log.Fatalf("Failed to change directory to %s: %v", backendDir, err)
    }

    log.Printf("Swaggerドキュメントを生成中...")
    log.Printf("API情報を生成中、検索ディレクトリ:%s", backendDir)

    // OSに応じたスクリプトを作成して実行
    isWindows := runtime.GOOS == "windows"
    var cmd *exec.Cmd
    var scriptPath string

    if isWindows {
        // Windows用のPowerShellスクリプト
        scriptPath = filepath.Join(os.TempDir(), "run_swag.ps1")
        scriptContent := `
$env:PATH = "$env:PATH;$(go env GOPATH)\bin"
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/marketplace/main.go -o docs
`
        err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
        if err != nil {
            log.Fatalf("Failed to create temporary script: %v", err)
        }
        defer os.Remove(scriptPath)

        // PowerShellスクリプトを実行
        cmd = exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", scriptPath)
    } else {
        // Unix用のシェルスクリプト
        scriptPath = filepath.Join(os.TempDir(), "run_swag.sh")
        scriptContent := `#!/bin/bash
export PATH=$PATH:$(go env GOPATH)/bin
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/marketplace/main.go -o docs
`
        err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
        if err != nil {
            log.Fatalf("Failed to create temporary script: %v", err)
        }
        defer os.Remove(scriptPath)

        // シェルスクリプトを実行
        cmd = exec.Command("/bin/bash", scriptPath)
    }

    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr

    // PATH環境変数を適切に設定
    goPath := filepath.Join(os.Getenv("HOME"), "go", "bin")
    if isWindows {
        cmd.Env = append(os.Environ(), fmt.Sprintf("PATH=%s;%s", os.Getenv("PATH"), goPath))
    } else {
        cmd.Env = append(os.Environ(), fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), goPath))
    }

    log.Println("Swaggerドキュメントを生成中...")
    err = cmd.Run()
    if err != nil {
        log.Fatalf("Swaggerドキュメントの生成に失敗しました: %v", err)
    }

    log.Println("Swaggerドキュメントが正常に生成されました!")
}

Swaggerドキュメントを生成するには、以下のコマンドを実行します:

cd app/backend
go run cmd/gen/generate_swagger.go

2.8 テスト環境の構築

2.8.1 ユニットテストの実装

ユニットテストを実装します:

// internal/domain/entities/product_test.go
package entities

import (
    "testing"
    "time"
)

func TestProduct(t *testing.T) {
    product := Product{
        Id:          "test-id",
        Name:        "Test Product",
        Description: "This is a test product",
        Price:       100.0,
        SellerId:    "seller-id",
        CreatedAt:   time.Now(),
        UpdatedAt:   time.Now(),
    }

    if product.Id != "test-id" {
        t.Errorf("Expected Id to be 'test-id', got '%s'", product.Id)
    }

    if product.Name != "Test Product" {
        t.Errorf("Expected Name to be 'Test Product', got '%s'", product.Name)
    }

    if product.Price != 100.0 {
        t.Errorf("Expected Price to be 100.0, got '%f'", product.Price)
    }
}

2.8.2 統合テストの実装

Testcontainersを使用してサービスの統合テストを実装します:

// internal/application/services/product_service_integration_test.go
package services

import (
    "context"
    "fmt"
    "testing"

    "github.com/google/uuid"
    "github.com/sklinkert/go-ddd/internal/application/command"
    "github.com/sklinkert/go-ddd/internal/application/common"
    "github.com/sklinkert/go-ddd/internal/application/interfaces"
    "github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
    "github.com/stretchr/testify/assert"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/wait"
    pgdriver "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "time"
)

func setupTestDatabase(t *testing.T) (*gorm.DB, func()) {
    ctx := context.Background()

    // Define PostgreSQL container
    pgReq := testcontainers.ContainerRequest{
        Image:        "postgres:15-alpine",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER":     "postgres",
            "POSTGRES_PASSWORD": "postgres",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections").
            WithOccurrence(2).WithStartupTimeout(5 * time.Second),
    }

    // Start PostgreSQL container
    pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: pgReq,
        Started:          true,
    })
    if err != nil {
        t.Fatalf("Failed to start PostgreSQL container: %s", err)
    }

    // Get container host and port
    host, err := pgContainer.Host(ctx)
    if err != nil {
        t.Fatalf("Failed to get PostgreSQL container host: %s", err)
    }

    port, err := pgContainer.MappedPort(ctx, "5432")
    if err != nil {
        t.Fatalf("Failed to get PostgreSQL container port: %s", err)
    }

    // Create connection string
    dsn := fmt.Sprintf("host=%s port=%s user=postgres password=postgres dbname=testdb sslmode=disable", host, port.Port())

    // Connect to the PostgreSQL database
    database, err := gorm.Open(pgdriver.Open(dsn), &gorm.Config{})
    if err != nil {
        t.Fatalf("Failed to connect to database: %s", err)
    }

    // AutoMigrate our models
    err = database.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
    if err != nil {
        t.Fatalf("Failed to migrate database: %s", err)
    }

    // Cleanup function
    cleanup := func() {
        // Clean up database
        database.Exec("DELETE FROM products")
        database.Exec("DELETE FROM sellers")

        // Stop and remove PostgreSQL container
        if err := pgContainer.Terminate(ctx); err != nil {
            t.Fatalf("Failed to terminate container: %s", err)
        }
    }

    return database, cleanup
}

func createTestSeller(t *testing.T, sellerService interfaces.SellerService) *common.SellerResult {
    sellerName := "Test Seller " + uuid.New().String()
    result, err := sellerService.CreateSeller(&command.CreateSellerCommand{
        Name: sellerName,
    })
    assert.NoError(t, err)
    assert.NotNil(t, result)
    assert.NotNil(t, result.Result)
    assert.Equal(t, sellerName, result.Result.Name)
    return result.Result
}

func TestProductService_Integration_CreateProduct(t *testing.T) {
    // Setup test database with Testcontainers
    db, cleanup := setupTestDatabase(t)
    defer cleanup()

    // Create repositories
    productRepo := postgres.NewGormProductRepository(db)
    sellerRepo := postgres.NewGormSellerRepository(db)

    // Create services
    productService := NewProductService(productRepo, sellerRepo)
    sellerService := NewSellerService(sellerRepo)

    // Create a seller first
    seller := createTestSeller(t, sellerService)

    // Test creating a product
    productName := "Test Product"
    productPrice := 99.99
    createProductCmd := &command.CreateProductCommand{
        Name:     productName,
        Price:    productPrice,
        SellerId: seller.Id,
    }

    result, err := productService.CreateProduct(createProductCmd)
    assert.NoError(t, err)
    assert.NotNil(t, result)
    assert.NotNil(t, result.Result)
    assert.Equal(t, productName, result.Result.Name)
    assert.Equal(t, productPrice, result.Result.Price)
    assert.Equal(t, seller.Id, result.Result.Seller.Id)
}

2.8.3 受け入れテストの実装

Cucumberを使用した受け入れテストを実装します。日本語でフィーチャを記述するには、ファイルの先頭に # language: ja を追加します:

# features/product/product_controller.feature
# language: ja
フィーチャ: 商品コントローラーAPI
  APIを通じて商品を管理するために
  クライアントとして
  HTTPリクエストを介して商品の作成、読み取り、更新、削除ができる必要があります

  シナリオ: APIを介して新しい商品を作成する
    前提 APIのための商品詳細を持っています
      | 名前        | 価格  | 出品者ID                             |
      | テスト商品  | 10.99 | 00000000-0000-0000-0000-000000000001 |
    もし 商品詳細を含めて"/api/v1/products"にPOSTリクエストを送信します
    ならば レスポンスステータスコードは201であるべきです
    かつ レスポンスは作成された商品詳細を含むべきです

  シナリオ: APIを介してすべての商品を取得する
    前提 システムに商品があります
    もし "/api/v1/products"にGETリクエストを送信します
    ならば レスポンスステータスコードは200であるべきです
    かつ レスポンスは商品のリストを含むべきです

  シナリオ: APIを介してIDで商品を取得する
    前提 システムにID "00000000-0000-0000-0000-000000000001"の商品があります
    もし "/api/v1/products/00000000-0000-0000-0000-000000000001"にGETリクエストを送信します
    ならば レスポンスステータスコードは200であるべきです
    かつ レスポンスは商品詳細を含むべきです

コントローラーを使用したステップ実装の例:

// features/steps/controller_steps.go
package steps

import (
    "bytes"
    "encoding/json"
    "fmt"
    "github.com/cucumber/godog"
    "github.com/labstack/echo/v4"
    "net/http"
    "net/http/httptest"
    "strconv"
)

// ControllerContext はコントローラー関連のステップの状態を保持します
type ControllerContext struct {
    echoInstance *echo.Echo
    requestBody  map[string]interface{}
    response     *httptest.ResponseRecorder
}

// RegisterSteps はgodogスイートにコントローラーステップを登録します
func (c *ControllerContext) RegisterSteps(ctx *godog.ScenarioContext) {
    // 日本語ステップ定義
    ctx.Step(`^APIのための商品詳細を持っています$`, c.iHaveProductDetailsForAPI)
    ctx.Step(`^商品詳細を含めて"([^"]*)"にPOSTリクエストを送信します$`, c.iSendAPOSTRequestToWithTheProductDetails)
    ctx.Step(`^レスポンスステータスコードは(\d+)であるべきです$`, c.theResponseStatusCodeShouldBe)
    ctx.Step(`^レスポンスは作成された商品詳細を含むべきです$`, c.theResponseShouldContainTheCreatedProductDetails)
    ctx.Step(`^システムに商品があります$`, c.thereAreProductsInTheSystem)
    ctx.Step(`^"([^"]*)"にGETリクエストを送信します$`, c.iSendAGETRequestTo)
    ctx.Step(`^レスポンスは商品のリストを含むべきです$`, c.theResponseShouldContainAListOfProducts)
    ctx.Step(`^システムにID "([^"]*)"の商品があります$`, c.thereIsAProductWithIDInTheSystem)
    ctx.Step(`^レスポンスは商品詳細を含むべきです$`, c.theResponseShouldContainTheProductDetails)
}

func (c *ControllerContext) iHaveProductDetailsForAPI(table *godog.Table) error {
    c.requestBody = make(map[string]interface{})

    // テーブルから商品詳細を取得
    for i := 1; i < len(table.Rows); i++ {
        row := table.Rows[i]
        for j, cell := range row.Cells {
            header := table.Rows[0].Cells[j].Value
            // 日本語のヘッダーを英語のフィールド名にマッピング
            switch header {
            case "名前":
                c.requestBody["Name"] = cell.Value
            case "価格":
                price, _ := strconv.ParseFloat(cell.Value, 64)
                c.requestBody["Price"] = price
            case "出品者ID":
                c.requestBody["SellerId"] = cell.Value
            }
        }
    }
    return nil
}

func (c *ControllerContext) iSendAPOSTRequestToWithTheProductDetails(path string) error {
    // リクエストボディをJSONに変換
    jsonBody, _ := json.Marshal(c.requestBody)

    // HTTPリクエストを作成して送信
    req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(jsonBody))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    c.response = httptest.NewRecorder()
    c.echoInstance.ServeHTTP(c.response, req)

    return nil
}

// その他のメソッドも同様に実装

このように、コントローラーを使用した受け入れテストでは、実際のHTTPリクエストとレスポンスをシミュレートし、APIエンドポイントの動作をテストします。リポジトリを直接テストする代わりに、コントローラーを通じてアプリケーション全体の動作を検証します。

受け入れテストでは、モックの代わりにTestContainersを使用して実際のデータベースを使用したテストを行うことができます。これにより、より実際の環境に近いテストが可能になります。

// features/steps/controller_steps.go
func (c *ControllerContext) setupTestDatabase() error {
    // PostgreSQLコンテナを定義
    pgReq := testcontainers.ContainerRequest{
        Image:        "postgres:13",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_USER":     "testuser",
            "POSTGRES_PASSWORD": "testpass",
            "POSTGRES_DB":       "testdb",
        },
        WaitingFor: wait.ForLog("database system is ready to accept connections").
            WithOccurrence(2).WithStartupTimeout(5 * time.Second),
    }

    // PostgreSQLコンテナを起動
    container, err := testcontainers.GenericContainer(c.ctx, testcontainers.GenericContainerRequest{
        ContainerRequest: pgReq,
        Started:          true,
    })
    if err != nil {
        return fmt.Errorf("failed to start PostgreSQL container: %w", err)
    }
    c.container = container

    // コンテナのホストとポートを取得
    host, err := container.Host(c.ctx)
    if err != nil {
        return fmt.Errorf("failed to get PostgreSQL container host: %w", err)
    }

    port, err := container.MappedPort(c.ctx, "5432")
    if err != nil {
        return fmt.Errorf("failed to get PostgreSQL container port: %w", err)
    }

    // データベース接続文字列を作成
    dsn := fmt.Sprintf("host=%s port=%s user=testuser password=testpass dbname=testdb sslmode=disable", host, port.Port())

    // PostgreSQLデータベースに接続
    db, err := gorm.Open(pgdriver.Open(dsn), &gorm.Config{})
    if err != nil {
        return fmt.Errorf("failed to connect to database: %w", err)
    }
    c.db = db

    // モデルをマイグレーション
    err = db.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
    if err != nil {
        return fmt.Errorf("failed to migrate database: %w", err)
    }

    return nil
}

func (c *ControllerContext) setupController() error {
    // TestContainersを使用してテストデータベースをセットアップ
    if err := c.setupTestDatabase(); err != nil {
        return err
    }

    // リポジトリを作成
    productRepo := postgres.NewGormProductRepository(c.db)
    sellerRepo := postgres.NewGormSellerRepository(c.db)

    // サービスを作成
    c.productService = services.NewProductService(productRepo, sellerRepo)
    c.sellerService = services.NewSellerService(sellerRepo)

    // Echoインスタンスを作成
    c.echoInstance = echo.New()

    // 商品コントローラーを作成
    c.productController = rest.NewProductController(c.echoInstance, c.productService)

    // レスポンスレコーダーを初期化
    c.response = httptest.NewRecorder()

    return nil
}

// テスト後のクリーンアップ
func (c *ControllerContext) RegisterSteps(ctx *godog.ScenarioContext) {
    // ... 他のステップ定義 ...

    ctx.BeforeScenario(func(*godog.Scenario) {
        if err := c.setupController(); err != nil {
            panic(fmt.Sprintf("Failed to setup controller: %v", err))
        }
    })

    ctx.AfterScenario(func(*godog.Scenario, error) {
        // データベースをクリーンアップ
        if c.db != nil {
            c.db.Exec("DELETE FROM products")
            c.db.Exec("DELETE FROM sellers")
        }

        // PostgreSQLコンテナを停止して削除
        if c.container != nil {
            if err := c.container.Terminate(c.ctx); err != nil {
                fmt.Printf("Failed to terminate container: %v\n", err)
            }
        }
    })
}

このように、TestContainersを使用したBDDテストでは、実際のデータベースを使用してテストを行うことができます。これにより、モックを使用する場合よりも実際の環境に近いテストが可能になり、より信頼性の高いテスト結果を得ることができます。

2.9 アプリケーションの実行

セットアップが完了したら、アプリケーションを実行します:

# データベースを起動
docker-compose up -d postgres

# Swaggerドキュメントを生成
cd app/backend
go run cmd/gen/generate_swagger.go

# アプリケーションを実行
go run cmd/marketplace/main.go

アプリケーションが起動したら、以下のURLにアクセスしてSwagger UIを確認できます:

http://localhost:9090/swagger/index.html

3. ディレクトリ構造の詳細

完成したアプリケーションのディレクトリ構造は以下のようになります:

app/backend/
├── cmd/
│   ├── marketplace/
│   │   └── main.go
│   └── gen/
│       └── generate_swagger.go
├── docs/
│   ├── swagger.json
│   ├── swagger.yaml
│   └── docs.go
├── features/
│   ├── product.feature
│   └── steps/
│       └── product_steps.go
├── internal/
│   ├── domain/
│   │   ├── entities/
│   │   │   ├── product.go
│   │   │   └── seller.go
│   │   └── repositories/
│   │       ├── product_repository.go
│   │       └── seller_repository.go
│   ├── application/
│   │   └── services/
│   │       ├── product_service.go
│   │       └── seller_service.go
│   ├── infrastructure/
│   │   └── db/
│   │       └── postgres/
│   │           ├── product_repository.go
│   │           └── seller_repository.go
│   └── interface/
│       └── api/
│           └── rest/
│               ├── product_controller.go
│               └── seller_controller.go
├── go.mod
└── go.sum

4. まとめ

この文書では、Go-DDDマーケットプレイスアプリケーションのバックエンド部分(app/backend)におけるGoアプリケーションのセットアップ手順と必要なモジュールの設定方法について説明しました。

セットアップ手順は以下の通りです:

  1. プロジェクト構造の作成
  2. Go Modulesの初期化
  3. 必要なパッケージのインストール
  4. データベース設定
  5. アプリケーション構造の実装
  6. ドメインモデル
  7. リポジトリインターフェース
  8. リポジトリの実装
  9. アプリケーションサービス
  10. コントローラー
  11. メインアプリケーション
  12. Swaggerドキュメントの設定
  13. テスト環境の構築
  14. ユニットテスト
  15. 統合テスト
  16. 受け入れテスト
  17. アプリケーションの実行

このセットアップ手順に従うことで、ドメイン駆動設計(DDD)の原則に基づいたGoアプリケーションの開発環境を構築することができます。