7.2 KiB
7.2 KiB
| name | description |
|---|---|
| golang-clean-arch | 當你在撰寫 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) 的函式)。
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/中定義的型別。
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),可選欄位則使用指標。
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)來進行錯誤判斷。
var (
ErrNotFound = errors.New("找不到指定資料 (not found)")
)
repository/(實作層)
- 結構體名稱使用小寫(如
jobRepository),不對外公開。 - 提供兩種建構子:
NewXxx(會回傳 error)與MustXxx(發生錯誤時 panic)。 - 建構子應回傳
domain/repository中定義的介面,而非結構體指標。 - 將底層資料庫驅動回傳的查無資料錯誤轉換為
domain.ErrNotFound。
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)。
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。
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函式。
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)
在進行跨模組引用時,請使用具備明確語義的別名以避免衝突:
import (
jobDomainUseCase "myapp/internal/module/job/domain/usecase"
templateRepo "myapp/internal/module/template/domain/repository"
templateDomain "myapp/internal/module/template/domain"
)
新增模組檢查清單 (Checklist)
- 依照上方規範建立目錄結構。
- 介面先行:依序撰寫
domain/entity/→domain/repository/→domain/usecase/。 - 撰寫基礎支撐:完成
repository/實作以及usecase/mock/。 - 撰寫核心邏輯:完成
usecase/實作。 - 補齊測試:在
usecase/<名稱>_test.go中實作表驅動測試,並遵循 AC 命名規則。