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

7.2 KiB
Raw Blame History

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 結構體,不包含具體實作。
  • 每個介面方法應對應一組 XxxReqXxxResp
  • 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(會回傳 errorMustXxx(發生錯誤時 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)

  1. 依照上方規範建立目錄結構。
  2. 介面先行:依序撰寫 domain/entity/domain/repository/domain/usecase/
  3. 撰寫基礎支撐:完成 repository/ 實作以及 usecase/mock/
  4. 撰寫核心邏輯:完成 usecase/ 實作。
  5. 補齊測試:在 usecase/<名稱>_test.go 中實作表驅動測試,並遵循 AC 命名規則。