208 lines
7.2 KiB
Markdown
208 lines
7.2 KiB
Markdown
---
|
||
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 命名規則。
|