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

814 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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) 重新分解
```