opencode-workflow/design-idea/translate/skills/go-backend-dev/SKILL.md

814 lines
24 KiB
Markdown
Raw Normal View History

2026-04-08 23:53:15 +00:00
---
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/ ← 依賴 logicHTTP/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 Contextinternal/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: PascalCaseexported或 camelCaseinternal
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) 重新分解
```