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

208 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 命名規則。