--- name: go-backend-dev description: "Backend Agent 使用此技能實作 Golang 後端。根據實作計畫和 API 規格,使用 Domain-Driven + go-zero 風格架構和 TDD 流程產出 production-ready 程式碼。觸發時機:Task Breakdown 完成後(Stage 9)。" --- # /go-backend-dev — Golang 後端實作 Backend Agent 使用此技能實作 Golang 後端。 ## 職責 1. 根據實作計畫建立專案結構(Domain-Driven + go-zero 風格) 2. 使用 TDD 流程實作功能(Red-Green-Refactor) 3. 按垂直切片逐步交付(端到端,非逐層) 4. 實作 Domain / Usecase / Logic / Repository 各層 5. 撰寫單元測試和整合測試 ## 輸入 - 實作計畫 (`./plans/{feature}.md`) - API 規格 (`docs/api/{date}-{feature}.yaml`) - DB Schema (`docs/db/{date}-{feature}.sql`) ## 輸出 - Golang 程式碼結構 - 測試程式碼(單元測試 >= 80%,業務邏輯 >= 90%) - Protobuf 定義(如需 gRPC) ## 流程 ``` 讀取實作計畫 + API 規格 + DB Schema ↓ 識別垂直切片(每個切片 = 端到端功能) ↓ 對每個切片執行 TDD 循環: ├── RED: 寫測試 → 測試失敗 ├── GREEN: 寫最少程式碼 → 測試通過 └── REFACTOR: 重構 → 測試仍然通過 ↓ 切片內建構順序: domain (entity/value object/interface) → pkg/domain/usecase (介面) → pkg/domain/repository (介面) → pkg/usecase (實作) → pkg/mock (mock) → internal/logic (handler 邏輯) → pkg/repository (基礎設施實作) ↓ 所有切片完成 → 執行整合測試 ↓ 確認交付物檢查清單 ``` ### 步驟說明 **1. 讀取輸入** 同時閱讀三個文件: - 實作計畫:了解垂直切片分解和優先順序 - API 規格:了解端點、請求/回應結構 - DB Schema:了解資料表結構和關係 **2. 識別垂直切片** 不是水平切片(一層一層做),而是垂直切片(端到端): ``` ✅ 正確方式(垂直): 切片 1: 使用者註冊 (domain.entity + domain.usecase介面 + usecase實作 + logic + repository) 切片 2: 使用者登入 (同上) 切片 3: 使用者列表 (同上) ❌ 錯誤方式(水平): 階段 1: 所有 domain entities 階段 2: 所有 usecases 階段 3: 所有 logic handlers ``` **3. TDD 循環(每個切片)** 對每個切片,遵循 Red-Green-Refactor: ``` RED: 寫一個測試 → 測試失敗 GREEN: 寫最少的程式碼讓測試通過 → 測試通過 REFACTOR: 重構程式碼 → 測試仍然通過 ``` 切片內建構順序(由內而外): 1. `pkg/domain/entity/` — 定義 Entity 和 Value Object 2. `pkg/domain/member/` — 定義值物件和列舉(含測試) 3. `pkg/domain/usecase/` — 定義 Use Case 介面 4. `pkg/domain/repository/` — 定義 Repository 介面 5. `pkg/usecase/` — 實作業務邏輯(先寫測試) 6. `pkg/mock/` — 產生 mock 7. `internal/logic/` — Handler 邏輯 8. `pkg/repository/` — 基礎設施實作(含 DB 測試) **4. 測試** 每個切片完成後,確保: - 單元測試通過(`pkg/usecase/*_test.go`) - 值物件測試通過(`pkg/domain/member/*_test.go`) - Repository 測試通過(`pkg/repository/*_test.go`) - 整合測試通過(關鍵路徑) - 測試覆蓋率達標 **5. 完成驗證** 最後確認所有交付物完整。 ## 專案結構 ``` project-root/ ├── build/ │ └── Dockerfile # 建置映像 │ ├── etc/ │ └── {service}.example.yaml # 範例設定檔 │ ├── generate/ │ └── protobuf/ │ └── {service}.proto # Protobuf 定義(如需 gRPC) │ ├── internal/ # 應用層(不對外暴露) │ ├── config/ │ │ └── config.go # 應用配置 │ ├── logic/ │ │ └── {module}/ │ │ ├── create_{entity}_logic.go # 每個 use case 一個 logic 檔案 │ │ ├── get_{entity}_logic.go │ │ ├── update_{entity}_logic.go │ │ └── ... │ ├── server/ │ │ └── {module}/ │ │ └── {module}_server.go # Server 定義(HTTP/gRPC) │ └── svc/ │ └── service_context.go # 依賴注入容器 │ ├── pkg/ # 領域層(可對外暴露) │ ├── domain/ │ │ ├── config/ │ │ │ └── config.go # Domain 配置 │ │ ├── entity/ │ │ │ ├── {entity}.go # Entity 定義 │ │ │ ├── {entity}_uid_table.go # UID 對照表 │ │ │ └── auto_id.go # 自動 ID 產生 │ │ ├── {module}/ │ │ │ ├── {value_object}.go # 值物件和列舉 │ │ │ └── {value_object}_test.go # 值物件測試 │ │ ├── repository/ │ │ │ ├── {entity}.go # Repository 介面 │ │ │ └── ... │ │ ├── usecase/ │ │ │ ├── {module}.go # Use Case 介面 │ │ │ └── ... │ │ ├── errors.go # Domain sentinel errors │ │ ├── const.go # Domain 常數 │ │ └── redis.go # Redis domain 定義 │ ├── mock/ │ │ ├── repository/ │ │ │ ├── {entity}.go # Repository mock │ │ │ └── ... │ │ └── usecase/ │ │ └── {module}.go # Use Case mock │ ├── repository/ │ │ ├── {entity}.go # Repository 實作 │ │ ├── {entity}_test.go # Repository 測試 │ │ ├── {entity}_uid.go # UID Repository 實作 │ │ ├── {entity}_uid_test.go │ │ ├── error.go # Repository 錯誤定義 │ │ └── start_{db}_container_test.go # testcontainers 啟動 │ └── usecase/ │ ├── {module}.go # Use Case 實作 │ ├── {operation}.go # 特定操作 │ ├── {operation}_test.go # Use Case 測試 │ └── {utils}.go # 工具函式 │ ├── {service}.go # 應用程式進入點 ├── Makefile ├── go.mod ├── go.sum ├── docker-compose.yml └── readme.md ``` ## 依賴方向規則 ``` pkg/domain/ ← 無外部依賴(最內層,純定義) ↑ pkg/domain/usecase/ ← Use Case 介面(只有介面定義) pkg/domain/repository/ ← Repository 介面(只有介面定義) ↑ pkg/usecase/ ← 依賴 domain 介面(業務邏輯實作) pkg/mock/ ← 依賴 domain 介面(測試 mock) ↑ internal/logic/ ← 依賴 usecase 實作(handler 邏輯) internal/server/ ← 依賴 logic(HTTP/gRPC server) internal/svc/ ← 依賴所有(DI 容器,組裝依賴) ↑ pkg/repository/ ← 依賴 domain 介面(基礎設施實作) ``` ``` ┌─────────────────────────────────┐ │ pkg/domain/ │ ← 純定義,無依賴 │ ├── entity/ │ │ ├── {module}/ (value objects)│ │ ├── repository/ (interfaces) │ │ ├── usecase/ (interfaces) │ │ ├── errors.go │ │ └── const.go │ ├─────────────────────────────────┤ │ pkg/usecase/ │ ← 依賴 domain 介面 │ pkg/mock/ │ ← 依賴 domain 介面 ├─────────────────────────────────┤ │ internal/logic/ │ ← 依賴 usecase │ internal/server/ │ │ internal/config/ │ │ internal/svc/ │ ← DI 容器 ├─────────────────────────────────┤ │ pkg/repository/ │ ← 依賴 domain 介面 └─────────────────────────────────┘ ``` ## 架構原則 ### `pkg/domain/` — 純領域定義 `pkg/domain/` 是核心,只包含**介面和定義**,不包含實作: ```go // pkg/domain/entity/user.go — Entity 定義 package entity type User struct { ID string `json:"id"` Email string `json:"email"` Name string `json:"name"` Status string `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } ``` ```go // pkg/domain/member/status.go — 值物件(含測試) package member type Status string const ( StatusActive Status = "active" StatusInactive Status = "inactive" ) func (s Status) IsValid() bool { switch s { case StatusActive, StatusInactive: return true } return false } func NewStatus(s string) (Status, error) { status := Status(s) if !status.IsValid() { return "", fmt.Errorf("invalid status: %s", s) } return status, nil } ``` ```go // pkg/domain/member/status_test.go — 值物件測試 package member func TestStatus_IsValid(t *testing.T) { assert.True(t, StatusActive.IsValid()) assert.True(t, StatusInactive.IsValid()) assert.False(t, Status("unknown").IsValid()) } func TestNewStatus(t *testing.T) { status, err := NewStatus("active") assert.NoError(t, err) assert.Equal(t, StatusActive, status) _, err = NewStatus("unknown") assert.Error(t, err) } ``` ```go // pkg/domain/repository/user.go — Repository 介面 package repository type UserRepository interface { GetByID(ctx context.Context, id string) (*entity.User, error) GetByEmail(ctx context.Context, email string) (*entity.User, error) Create(ctx context.Context, user *entity.User) error Update(ctx context.Context, user *entity.User) error Delete(ctx context.Context, id string) error } ``` ```go // pkg/domain/usecase/user.go — Use Case 介面 package usecase type UserUsecase interface { CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) GetUser(ctx context.Context, id string) (*entity.User, error) UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*entity.User, error) } ``` ```go // pkg/domain/errors.go — Domain sentinel errors package domain import "errors" var ( ErrUserNotFound = errors.New("user not found") ErrInvalidInput = errors.New("invalid input") ErrDuplicateEmail = errors.New("email already exists") ) ``` ### `pkg/usecase/` — 業務邏輯實作 每個檔案一個功能領域,測試檔案同目錄: ```go // pkg/usecase/account.go — Use Case 進入點 package usecase type AccountUsecase struct { userRepo repository.UserRepository accountRepo repository.AccountRepository redis *redis.Client } func NewAccountUsecase( userRepo repository.UserRepository, accountRepo repository.AccountRepository, redis *redis.Client, ) *AccountUsecase { return &AccountUsecase{ userRepo: userRepo, accountRepo: accountRepo, redis: redis, } } ``` ```go // pkg/usecase/create_user.go — 單一操作 package usecase func (uc *AccountUsecase) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) { if err := input.Validate(); err != nil { return nil, fmt.Errorf("validate input: %w", err) } existing, _ := uc.userRepo.GetByEmail(ctx, input.Email) if existing != nil { return nil, domain.ErrDuplicateEmail } user, err := entity.NewUser(input.Email, input.Password, input.Name) if err != nil { return nil, fmt.Errorf("create user: %w", err) } if err := uc.userRepo.Create(ctx, user); err != nil { return nil, fmt.Errorf("save user: %w", err) } return user, nil } ``` ```go // pkg/usecase/create_user_test.go — 測試同目錄 package usecase func TestAccountUsecase_CreateUser_Success(t *testing.T) { mockUserRepo := new(mock.UserRepository) uc := NewAccountUsecase(mockUserRepo, nil, nil) mockUserRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil) mockUserRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil) user, err := uc.CreateUser(context.Background(), input) assert.NoError(t, err) assert.NotNil(t, user) mockUserRepo.AssertExpectations(t) } ``` ### `pkg/mock/` — 自動產生的 Mock ```go // pkg/mock/repository/user.go — 由 mockery 產生 //go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository package mock_repository import ( "github.com/stretchr/testify/mock" "your-project/pkg/domain/repository" ) type UserRepository struct { mock.Mock } func (m *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) { args := m.Called(ctx, id) if args.Get(0) == nil { return nil, args.Error(1) } return args.Get(0).(*entity.User), args.Error(1) } ``` ### `internal/logic/` — Handler 邏輯 每個 use case 一個 logic 檔案(go-zero 風格): ```go // internal/logic/account/create_user_logic.go package account type CreateUserLogic struct { ctx context.Context svcCtx *svc.ServiceContext } func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic { return &CreateUserLogic{ ctx: ctx, svcCtx: svcCtx, } } func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) (*types.UserResp, error) { user, err := l.svcCtx.UserUsecase.CreateUser(l.ctx, usecase.CreateUserInput{ Email: req.Email, Password: req.Password, Name: req.Name, }) if err != nil { return nil, err } return &types.UserResp{ ID: user.ID, Email: user.Email, Name: user.Name, }, nil } ``` ### `internal/svc/` — 依賴注入容器 ```go // internal/svc/service_context.go package svc type ServiceContext struct { Config config.Config UserUsecase usecase.UserUsecase AccountUsecase usecase.AccountUsecase } func NewServiceContext(c config.Config) *ServiceContext { db := mongo.NewClient(c.Mongo.URI) redisClient := redis.NewClient(c.Redis) userRepo := repository.NewUserRepository(db) accountRepo := repository.NewAccountRepository(db) return &ServiceContext{ Config: c, UserUsecase: usecase.NewUserUsecase(userRepo, redisClient), AccountUsecase: usecase.NewAccountUsecase(userRepo, accountRepo, redisClient), } } ``` ### `pkg/repository/` — 基礎設施實作 ```go // pkg/repository/user.go package repository type userRepository struct { db *mongo.Database } func NewUserRepository(db *mongo.Database) domain.Repository.UserRepository { return &userRepository{db: db} } func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) { var user entity.User err := r.db.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&user) if err != nil { if err == mongo.ErrNoDocuments { return nil, domain.ErrUserNotFound } return nil, fmt.Errorf("find user by id: %w", err) } return &user, nil } ``` ```go // pkg/repository/user_test.go package repository func TestUserRepository_GetByID_Success(t *testing.T) { db := startMongoContainer(t) defer db.Client().Disconnect(context.Background()) repo := NewUserRepository(db) // ... } ``` ## 編碼規範 ### 檔案命名 ``` 值物件和列舉: pkg/domain/{module}/{name}.go + _test.go Entity: pkg/domain/entity/{name}.go Repository 介面:pkg/domain/repository/{name}.go Usecase 介面: pkg/domain/usecase/{module}.go Usecase 實作: pkg/usecase/{operation}.go + _test.go Usecase 工具: pkg/usecase/{module}_utils.go + _test.go Repository 實作:pkg/repository/{name}.go + _test.go Mock: pkg/mock/repository/{name}.go pkg/mock/usecase/{module}.go Handler 邏輯: internal/logic/{module}/{operation}_logic.go Server: internal/server/{module}/{module}_server.go Service Context:internal/svc/service_context.go Protobuf 定義: generate/protobuf/{module}.proto 設定檔: etc/{service}.yaml Dockerfile: build/Dockerfile ``` ### 命名規範 ```go // Package: 小寫,語意明確 package usecase // 不是 usecases package repository // 不是 repositories package entity // 不是 entities // Entity struct: PascalCase,無後綴 type User struct { ... } // 不是 UserModel, UserEntity // Value Object: 基礎型別別名 + 方法 type Status string // 不是 StatusEnum // Interface (介面): 放在 pkg/domain/ 下,語意命名 type UserRepository interface { ... } // 不是 UserRepo 或 UserRepositoryI // Use Case struct: {Module}Usecase type AccountUsecase struct { ... } // Use Case 方法: 動詞開頭 func (uc *AccountUsecase) CreateUser(ctx context.Context, ...) (*entity.User, error) func (uc *AccountUsecase) GetUser(ctx context.Context, id string) (*entity.User, error) // Logic struct: {Operation}Logic type CreateUserLogic struct { ... } // Error: Err 前綴 var ErrUserNotFound = errors.New("user not found") // Constant: PascalCase(exported)或 camelCase(internal) const MaxRetryCount = 3 const defaultPageSize = 20 ``` ### 錯誤處理 ```go // Sentinel errors — pkg/domain/errors.go var ( ErrUserNotFound = errors.New("user not found") ErrInvalidInput = errors.New("invalid input") ErrDuplicateEmail = errors.New("email already exists") ) // Error wrapping — always use %w if err != nil { return fmt.Errorf("create user: %w", err) } // Error checking — always use errors.Is if errors.Is(err, domain.ErrUserNotFound) { // handle not found } // Repository errors — pkg/repository/error.go var ( ErrMongoConnection = errors.New("mongo connection failed") ErrRedisConnection = errors.New("redis connection failed") ) ``` ### 介面設計 ```go // 介面定義在 pkg/domain/(消費者端) // 實作定義在 pkg/ 下(提供者端) // Accept interfaces, return structs func NewUserUsecase(repo repository.UserRepository, redis *redis.Client) *UserUsecase { return &UserUsecase{repo: repo, redis: redis} } // Keep interfaces small (1-3 methods) type UserRepository interface { GetByID(ctx context.Context, id string) (*entity.User, error) Create(ctx context.Context, user *entity.User) error } ``` ## TDD 規範 此技能與 `tdd` 技能整合,遵循共同的 TDD 原則。 ### 測試金字塔 ``` /\ / \ / E2E \ <- 少數關鍵流程 /--------\ /Integration\ <- DB + Redis (testcontainers) /--------------\ / Unit Tests \ <- 最多,80%+ 覆蓋 /--------------------\ ``` ### 測試位置 ``` 測試檔案跟原始碼同目錄: pkg/domain/member/status_test.go ← 值物件測試 pkg/usecase/create_user_test.go ← Use Case 測試 pkg/repository/user_test.go ← Repository 測試 pkg/repository/start_mongo_container_test.go ← testcontainers 啟動 ``` ### 垂直切片 TDD 每個切片按照以下順序: ``` 切片: 使用者註冊 1. RED: 寫 TestStatus_IsValid (值物件) 2. GREEN: 寫 Status.IsValid() 3. RED: 寫 TestAccountUsecase_CreateUser_Success 4. GREEN: 寫 domain/entity, domain/usecase介面, pkg/usecase實作, mock 5. RED: 寫 TestAccountUsecase_CreateUser_DuplicateEmail 6. GREEN: 加入重複檢查 7. RED: 寫 TestUserRepository_Create (DB 測試) 8. GREEN: 寫 pkg/repository/user.go 9. RED: 寫 TestCreateUserLogic (handler 測試) 10. GREEN: 寫 internal/logic/account/create_user_logic.go 11. REFACTOR: 清理全部 ``` ### Mock 策略 ```go // 使用 mockery 自動產生 mock // 在接口檔案加上 go:generate 指令 //go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository // 單元測試使用 mock func TestAccountUsecase_CreateUser_Success(t *testing.T) { mockRepo := new(mock_repository.UserRepository) uc := usecase.NewAccountUsecase(mockRepo, nil, nil) mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil) mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil) user, err := uc.CreateUser(context.Background(), input) assert.NoError(t, err) assert.NotNil(t, user) } ``` ### testcontainers 策略 ```go // pkg/repository/start_mongo_container_test.go func startMongoContainer(t *testing.T) *mongo.Database { ctx := context.Background() req := testcontainers.ContainerRequest{ Image: "mongo:7", ExposedPorts: []string{"27017/tcp"}, WaitingFor: wait.ForListeningPort("27017/tcp"), } mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) require.NoError(t, err) t.Cleanup(func() { mongoC.Terminate(ctx) }) // ... return connected database } ``` ### 覆蓋率要求 - 值物件 (`pkg/domain/member/`): >= 90% - Use Case (`pkg/usecase/`): >= 90% - Repository (`pkg/repository/`): >= 80% - Logic (`internal/logic/`): >= 80%(整合測試為主) - Critical paths: Integration tests required ## 垂直切片模板 每個垂直切片的檔案清單: ``` 切片: {operation}_{entity} 新增/修改的檔案: ├── pkg/domain/entity/{entity}.go ← Entity 定義 ├── pkg/domain/member/{value_object}.go ← 值物件(如需) ├── pkg/domain/member/{value_object}_test.go ← 值物件測試 ├── pkg/domain/repository/{entity}.go ← Repository 介面 ├── pkg/domain/usecase/{module}.go ← Use Case 介面 ├── pkg/usecase/{operation}.go ← Use Case 實作 ├── pkg/usecase/{operation}_test.go ← Use Case 測試 ├── pkg/mock/repository/{entity}.go ← Repository mock ├── pkg/repository/{entity}.go ← Repository 實作 ├── pkg/repository/{entity}_test.go ← Repository 測試 ├── internal/logic/{module}/{operation}_logic.go ← Handler 邏輯 └── internal/svc/service_context.go ← 更新 DI ``` ## 完成檢查清單 ### 每個切片完成後 - [ ] 值物件測試通過 - [ ] Use Case 測試通過 - [ ] Repository 測試通過(含 DB) - [ ] 錯誤處理完整 - [ ] 依賴方向正確(domain 無外部依賴) ### 全部完成後 - [ ] 專案結構符合 Domain-Driven + go-zero 風格 - [ ] `pkg/domain/` 包含所有 Entity、Value Object、介面定義 - [ ] `pkg/usecase/` 包含所有業務邏輯實作 - [ ] `pkg/repository/` 包含所有基礎設施實作 - [ ] `internal/logic/` 包含所有 Handler 邏輯 - [ ] `internal/svc/` 包含完整的依賴注入設定 - [ ] 單元測試 >= 80% 覆蓋率 - [ ] 業務邏輯 >= 90% 覆蓋率 - [ ] 整合測試通過(關鍵路徑) - [ ] 錯誤處理一致且使用 `%w` wrapping ## 相依技能 - **前置**: `prd-to-plan` (實作計畫), `be-api-design` (API 規格), `dba-schema` (DB Schema) - **輔助**: `tdd` (TDD Red-Green-Refactor 流程), `design-an-interface` (介面設計) - **後續**: `qa` (QA 測試) ## 退回機制 ``` QA 失敗 (Stage 10) ↓ Orchestrator 重新分配修復任務 ↓ Backend Agent 修復 Bug + 新增回歸測試 ↓ 重新進入 QA (Stage 10) Code Review 退回 (Stage 11) ↓ 處理 PR 回饋 ↓ 重新進入 QA (Stage 10) 驗證 實作計畫不可行 ↓ 退回 Task Breakdown (Stage 8) 重新分解 ```