claude-code/claude-zh/skills/golang-clean-arch/SKILL.md

208 lines
7.2 KiB
Markdown
Raw Normal View History

2026-02-27 13:45:37 +00:00
---
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 命名規則。