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

24 KiB
Raw Blame History

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 後端。

職責

  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/ 是核心,只包含介面和定義,不包含實作:

// 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 Contextinternal/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: PascalCaseexported或 camelCaseinternal
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% 覆蓋率
  • 整合測試通過(關鍵路徑)
  • 錯誤處理一致且使用 %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) 重新分解