--- name: golang-clean-arch description: 當你在撰寫 Go 的新模組、新功能或新服務時,請遵循此 Clean Architecture(潔淨架構)目錄結構與分層規範。此架構由實戰經驗提煉而成。 --- # Go Clean Architecture 模組結構規範 ## 目錄結構 所有業務模組應路徑應為 `internal/module/<模組名稱>/`,其內部結構如下: ``` internal/module/<模組名稱>/ ├── const.go # 模組層級常數(Package 名稱使用模組名) ├── domain/ │ ├── errors.go # 哨兵錯誤 (Sentinel Errors,如 ErrNotFound 等) │ ├── constants.go # 領域層 (Domain) 常數(如狀態值字串等) │ ├── entity/ # 資料結構(對應資料庫資料表) │ │ ├── <名稱>.go │ │ └── <名稱>_helper.go # 與實體 (Entity) 相關的純函式 (Pure Functions) │ ├── repository/ # 倉儲 (Repository) 介面定義 │ │ └── <名稱>.go │ └── usecase/ # 使用案例 (UseCase) 介面與 Req/Resp 型別定義 │ └── <名稱>.go ├── repository/ # 倉儲實作(依賴資料庫驅動程式) │ ├── <名稱>.go │ ├── <名稱>_test.go │ └── test_helper.go └── usecase/ # 使用案例實作(處理業務邏輯) ├── <名稱>.go ├── <名稱>_test.go └── mock/ └── mock_repositories.go # 手寫的 Mock 物件,供 UseCase 測試使用 ``` ## 各層分工與職責 ### `domain/entity/` - 對應資料庫資料表 (DB Table) 的結構體 (Struct),僅包含欄位與 `TableName()` 方法。 - 除了資料庫驅動程式的特定型別(如 `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/` - **僅定義介面與 Req/Resp 結構體**,不包含具體實作。 - 每個介面方法應對應一組 `XxxReq` 與 `XxxResp`。 - Req/Resp 應使用數值型別 (Value Type) 而非指標 (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` - 僅定義哨兵錯誤,建議使用 `errors.New`。 - UseCase 實作層應使用 `errors.Is(err, domain.ErrNotFound)` 來進行錯誤判斷。 ```go var ( ErrNotFound = errors.New("找不到指定資料 (not found)") ) ``` ### `repository/`(實作層) - 結構體名稱使用小寫(如 `jobRepository`),不對外公開。 - 提供兩種建構子:`NewXxx`(會回傳 error)與 `MustXxx`(發生錯誤時 panic)。 - 建構子應回傳 **`domain/repository` 中定義的介面**,而非結構體指標。 - 將底層資料庫驅動回傳的查無資料錯誤轉換為 `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("建立 Job Repository 失敗: %v", err)) } return r } ``` ### `usecase/`(實作層) - 結構體名稱使用小寫(如 `jobUseCase`),不對外公開。 - 採用依賴注入 (Dependency Injection):所需的 Repository 皆透過建構子傳入(介面型別)。 - 建構子應回傳 **`domain/usecase` 中定義的介面**。 - 錯誤處理:將 Domain 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 結構體應對應一個 Repository 介面。 - 進行型別斷言 (Type Assertion) 前應先判斷回傳值是否為 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/<名稱>_test.go`) - 套件名稱使用 `package usecase`(白箱測試)。 - 使用表驅動測試 (Table-Driven Tests),每個案例應對應一條驗收準則 (Acceptance Criteria, AC)。 - 命名慣例:`AC-序號: 情境描述` (如 `AC-J01: 成功取得 Job`)。 - 每個測試案例應配備獨立的 `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/<名稱>_test.go`) - 屬於整合測試 (Integration Test),需連線至真實資料庫。 - 共用 `test_helper.go` 以提供資料庫的 Setup 與 Teardown(環境建立與清理)。 ## 匯入別名對照表 (Alias Convention) 在進行跨模組引用時,請使用具備明確語義的別名以避免衝突: ```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/<名稱>_test.go` 中實作表驅動測試,並遵循 AC 命名規則。