24 KiB
24 KiB
| name | description |
|---|---|
| go-backend-dev | Backend Agent 使用此技能實作 Golang 後端。根據實作計畫和 API 規格,使用 Domain-Driven + go-zero 風格架構和 TDD 流程產出 production-ready 程式碼。觸發時機:Task Breakdown 完成後(Stage 9)。 |
/go-backend-dev — Golang 後端實作
Backend Agent 使用此技能實作 Golang 後端。
職責
- 根據實作計畫建立專案結構(Domain-Driven + go-zero 風格)
- 使用 TDD 流程實作功能(Red-Green-Refactor)
- 按垂直切片逐步交付(端到端,非逐層)
- 實作 Domain / Usecase / Logic / Repository 各層
- 撰寫單元測試和整合測試
輸入
- 實作計畫 (
./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: 重構程式碼 → 測試仍然通過
切片內建構順序(由內而外):
pkg/domain/entity/— 定義 Entity 和 Value Objectpkg/domain/member/— 定義值物件和列舉(含測試)pkg/domain/usecase/— 定義 Use Case 介面pkg/domain/repository/— 定義 Repository 介面pkg/usecase/— 實作業務邏輯(先寫測試)pkg/mock/— 產生 mockinternal/logic/— Handler 邏輯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/ 是核心,只包含介面和定義,不包含實作:
// 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"`
}
// 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
}
// 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)
}
// 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
}
// 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)
}
// 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/ — 業務邏輯實作
每個檔案一個功能領域,測試檔案同目錄:
// 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,
}
}
// 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
}
// 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
// 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 風格):
// 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/ — 依賴注入容器
// 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/ — 基礎設施實作
// 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
}
// 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
命名規範
// 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
錯誤處理
// 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")
)
介面設計
// 介面定義在 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 策略
// 使用 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 策略
// 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% 覆蓋率
- 整合測試通過(關鍵路徑)
- 錯誤處理一致且使用
%wwrapping
相依技能
- 前置:
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) 重新分解