---
name: golang-clean-arch
description: 當你在寫 Go 的新模組、新功能、或新 service 時，使用這套 Clean Architecture 目錄結構與分層規範。這是從 orbit-jobs-manager 專案提煉出來的實戰架構。
---

# Go Clean Architecture 模組結構

## 目錄結構

每個業務模組放在 `internal/module/<模組名>/` 下，結構如下：

```
internal/module/<name>/
├── const.go                        # 模組層級常數（package 名用模組名）
├── domain/
│   ├── errors.go                   # sentinel errors（ErrNotFound 等）
│   ├── constants.go                # domain 層常數（狀態值字串等）
│   ├── entity/                     # 資料結構（對應 DB table）
│   │   ├── <name>.go
│   │   └── <name>_helper.go        # entity 相關的純函式（FlattenSteps 等）
│   ├── repository/                 # Repository 介面定義
│   │   └── <name>.go
│   └── usecase/                    # UseCase 介面 + Req/Resp 型別定義
│       └── <name>.go
├── repository/                     # Repository 實作（依賴 DB driver）
│   ├── <name>.go
│   ├── <name>_test.go
│   └── test_helper.go
└── usecase/                        # UseCase 實作（業務邏輯）
    ├── <name>.go
    ├── <name>_test.go
    └── mock/
        └── mock_repositories.go    # 手寫 mock，供 usecase 測試用
```

## 各層職責

### `domain/entity/`
- 對應 DB table 的 struct，只有欄位和 `TableName()` 方法
- 不依賴任何外部套件（除了 DB driver 的型別如 `gocql.UUID`）
- helper 檔放純函式（不帶 receiver 的工具函式）

```go
type Job struct {
    JobID   gocql.UUID `db:"job_id" partition_key:"true"`
    Status  string     `db:"status"`
    // ...
}

func (j Job) TableName() string { return "jobs" }
```

### `domain/repository/`
- **只有 interface**，不含任何實作
- 方法簽名用 `context.Context` 作第一個參數
- 依賴 `domain/entity/` 的型別

```go
type JobRepository interface {
    Create(ctx context.Context, job *entity.Job) error
    Get(ctx context.Context, jobID string) (*entity.Job, error)
    Update(ctx context.Context, job *entity.Job) error
}
```

### `domain/usecase/`
- **只有 interface + Req/Resp struct**，不含任何實作
- 每個方法對應一個 `XxxReq` 和 `XxxResp`
- Req/Resp 用 value type（不是 pointer），可選欄位用 pointer

```go
type GetJobReq struct {
    JobID string
}

type GetJobResp struct {
    Job   *entity.Job
    Steps []entity.JobStep
}

type JobUseCase interface {
    GetJob(ctx context.Context, req GetJobReq) (*GetJobResp, error)
}
```

### `domain/errors.go`
- 只放 sentinel errors，用 `errors.New`
- usecase 實作層用 `errors.Is(err, domain.ErrNotFound)` 判斷

```go
var (
    ErrNotFound = errors.New("not found")
)
```

### `repository/`（實作層）
- struct 名用小寫（`jobRepository`），不對外暴露
- 建構子提供兩種：`NewXxx`（回傳 error）和 `MustXxx`（panic on error）
- 建構子回傳 **domain/repository 的 interface**，不是 struct
- 將 DB 層的 not found 錯誤轉換為 `domain.ErrNotFound`

```go
func NewJobRepository(db *cassandra.DB, keyspace string) (repository.JobRepository, error) {
    // ...
    return &jobRepository{...}, nil
}

func MustJobRepository(param JobRepositoryParam) repository.JobRepository {
    r, err := NewJobRepository(param.DB, param.Keyspace)
    if err != nil {
        panic(fmt.Sprintf("failed to create job repository: %v", err))
    }
    return r
}
```

### `usecase/`（實作層）
- struct 名用小寫（`jobUseCase`），不對外暴露
- 依賴注入：所有 repository 都透過建構子傳入（interface 型別）
- 建構子回傳 **domain/usecase 的 interface**
- 錯誤處理：domain error → 轉換為 application error（如 `errs.ResNotFoundError`）

```go
type jobUseCase struct {
    jobRepo     repository.JobRepository
    templateRepo templateRepo.TemplateRepository
}

func NewJobUseCase(
    jobRepo repository.JobRepository,
    templateRepo templateRepo.TemplateRepository,
) domainUseCase.JobUseCase {
    return &jobUseCase{
        jobRepo:      jobRepo,
        templateRepo: templateRepo,
    }
}
```

### `usecase/mock/`
- 手寫 mock（用 `github.com/stretchr/testify/mock`）
- 檔案開頭加 `// Code generated by hand for testing. DO NOT EDIT.`
- 每個 mock struct 對應一個 repository interface
- nil 回傳值要做型別斷言前先判斷

```go
type MockJobRepository struct {
    mock.Mock
}

func (m *MockJobRepository) Get(ctx context.Context, jobID string) (*entity.Job, error) {
    args := m.Called(ctx, jobID)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*entity.Job), args.Error(1)
}
```

## 測試規範

### usecase 測試（`usecase/<name>_test.go`）
- package 用 `package usecase`（白箱測試）
- 用 table-driven tests，每個 case 對應一個 AC（驗收條件）
- 命名格式：`AC-X01: 描述`
- 每個 test case 有獨立的 `setupMocks` 函式

```go
tests := []struct {
    name       string
    req        domainUseCase.GetJobReq
    setupMocks func(*mock.MockJobRepository)
    wantErr    bool
}{
    {
        name: "AC-J01: 成功取得 job",
        req:  domainUseCase.GetJobReq{JobID: "xxx"},
        setupMocks: func(jobRepo *mock.MockJobRepository) {
            jobRepo.On("Get", ctx, "xxx").Return(&entity.Job{...}, nil)
        },
    },
}
```

### repository 測試（`repository/<name>_test.go`）
- 整合測試，需要真實 DB 連線
- 共用 `test_helper.go` 提供 DB setup/teardown

## Import 別名慣例

跨模組引用時，用有意義的別名避免衝突：

```go
import (
    jobDomainUseCase "myapp/internal/module/job/domain/usecase"
    templateRepo     "myapp/internal/module/template/domain/repository"
    templateDomain   "myapp/internal/module/template/domain"
)
```

## 新增模組 Checklist

1. 建立目錄結構（參考上方樹狀圖）
2. 先寫 `domain/entity/` → `domain/repository/` → `domain/usecase/`（介面先行）
3. 再寫 `repository/` 實作 + `usecase/mock/`
4. 最後寫 `usecase/` 實作
5. 同步補 `usecase/<name>_test.go`（table-driven + AC 命名）

