Files
es-demo/.github/instructions/DEV_GUIDELINE.instructions.md

24 KiB
Raw Permalink Blame History

applyTo
applyTo
**/*.go

AI Agent 开发指令

本文档为 AI AgentGitHub Copilot提供开发本项目时的详细指令和指南。

项目背景

这是一个用于测试和实验 AWS OpenSearch Service 的 Go 项目。项目目标是创建生产级别的代码,并为未来转化为可复用的依赖库做准备。

核心原则

1. 代码质量要求(最高优先级)

必须遵循 Go 官方最佳实践:

具体要求:

// ✅ 正确:导出的函数必须有文档注释,以函数名开头
// NewClient creates and initializes a new OpenSearch client with the provided configuration.
// It returns an error if the connection cannot be established.
func NewClient(cfg *Config) (*Client, error) {
    // ...
}

// ❌ 错误:缺少注释或注释不以函数名开头
func NewClient(cfg *Config) (*Client, error) {
    // ...
}
// ✅ 正确:错误处理不能忽略
result, err := client.Search(ctx, query)
if err != nil {
    return nil, fmt.Errorf("search failed: %w", err)
}

// ❌ 错误:忽略错误
result, _ := client.Search(ctx, query)
// ✅ 正确:使用有意义的变量名
var maxRetryCount = 3
var connectionTimeout = 30 * time.Second

// ❌ 错误:使用无意义的缩写
var mrc = 3
var ct = 30 * time.Second

2. 命名规范

包名:

  • 小写、简短、单数形式
  • 避免使用下划线或驼峰命名
  • 示例:clientsearchdocument(不是 opensearch_client

接口名:

  • 单方法接口以 -er 结尾:ReaderWriterSearcher
  • 多方法接口使用描述性名称:IndexManagerDocumentService

变量和函数名:

  • 驼峰命名camelCase 或 PascalCase
  • 缩写词保持一致大写:URLHTTPIDJSON(不是 UrlHttpIdJson
  • 示例:clientIDhttpClientparseURL

常量:

// ✅ 正确:使用驼峰命名
const DefaultTimeout = 30 * time.Second
const MaxRetries = 3

// 或者使用全大写(枚举类型)
const (
    StatusPending  = "PENDING"
    StatusComplete = "COMPLETE"
)

3. 错误处理规范

基本错误处理:

// ✅ 正确:包装错误,提供上下文信息
if err != nil {
    return fmt.Errorf("failed to create index %q: %w", indexName, err)
}

// ✅ 正确:自定义错误类型(当需要时)
var ErrIndexNotFound = errors.New("index not found")
var ErrInvalidQuery = errors.New("invalid query syntax")

// ✅ 正确:错误检查使用 errors.Is 或 errors.As
if errors.Is(err, ErrIndexNotFound) {
    // 处理索引不存在的情况
}

// ❌ 错误:吞掉错误信息
if err != nil {
    return errors.New("something went wrong")
}

Defer 中的错误处理:

对于 defer 语句中的 Close() 等清理操作,不能完全忽略错误:

// ✅ 正确:检查并记录 Close 错误
defer func() {
    if closeErr := res.Body.Close(); closeErr != nil {
        fmt.Fprintf(os.Stderr, "warning: failed to close response body: %v\n", closeErr)
    }
}()

// ❌ 错误:完全忽略错误(虽然能通过 linter但不是最佳实践
defer func() { _ = res.Body.Close() }()

// ❌ 错误不处理错误errcheck 警告)
defer res.Body.Close()

原因说明:

  • HTTP Response Body 的 Close() 错误虽然通常不影响业务逻辑(数据已读取)
  • 但完全忽略错误不符合 Go 最佳实践
  • 检查并记录到 stderr 便于调试和监控
  • 这种方式既满足 linter 要求,又提供诊断信息

### 4. 注释规范

```go
// ✅ 正确:包注释(在 package 语句之前)
// Package client provides a high-level interface for interacting with AWS OpenSearch Service.
// It handles connection management, authentication, and request signing.
package client

// ✅ 正确:导出类型的注释
// Client represents an OpenSearch client that manages connections and operations.
type Client struct {
    // ...
}

// ✅ 正确:导出方法的注释
// Search executes a search query against the specified index.
// It returns the search results or an error if the operation fails.
func (c *Client) Search(ctx context.Context, indexName string, query Query) (*SearchResult, error) {
    // ...
}

// ✅ 正确:复杂逻辑的内部注释
func processResults(data []byte) error {
    // Parse JSON response
    var resp Response
    if err := json.Unmarshal(data, &resp); err != nil {
        return err
    }
    
    // Validate response structure
    if resp.Total == 0 {
        return ErrNoResults
    }
    
    // Transform results
    // ...
}

5. 代码结构规范

文件组织:

// 1. 包声明和包注释
// Package client provides...
package client

// 2. 导入语句(分组:标准库、第三方库、本地包)
import (
    "context"
    "fmt"
    "time"
    
    "github.com/opensearch-project/opensearch-go/v2"
    
    "es-demo/config"
    "es-demo/models"
)

// 3. 常量
const (
    DefaultTimeout = 30 * time.Second
)

// 4. 变量
var (
    ErrInvalidConfig = errors.New("invalid configuration")
)

// 5. 类型定义
type Client struct {
    // ...
}

// 6. 构造函数
func NewClient(cfg *Config) (*Client, error) {
    // ...
}

// 7. 方法(接收者方法)
func (c *Client) Connect(ctx context.Context) error {
    // ...
}

// 8. 其他函数
func validateConfig(cfg *Config) error {
    // ...
}

接口设计:

// ✅ 正确:接口定义在使用方,而非实现方
// 在 operations/search 包中定义
type DocumentStore interface {
    GetDocument(ctx context.Context, id string) (*Document, error)
    PutDocument(ctx context.Context, doc *Document) error
}

// ✅ 正确:保持接口小而专注
type Searcher interface {
    Search(ctx context.Context, query Query) (*Result, error)
}

type Indexer interface {
    CreateIndex(ctx context.Context, name string, settings map[string]any) error
    DeleteIndex(ctx context.Context, name string) error
}

6. 测试规范

文件命名:

  • 测试文件:client_test.go(对应 client.go
  • 测试函数:TestNewClientTestClient_Search

测试结构:

func TestNewClient(t *testing.T) {
    // 使用表驱动测试
    tests := []struct {
        name    string
        config  *Config
        want    bool  // 期望成功/失败
        wantErr error
    }{
        {
            name: "valid config",
            config: &Config{
                Endpoint: "https://example.com",
            },
            want:    true,
            wantErr: nil,
        },
        {
            name:    "nil config",
            config:  nil,
            want:    false,
            wantErr: ErrInvalidConfig,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := NewClient(tt.config)
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if (got != nil) != tt.want {
                t.Errorf("NewClient() got = %v, want %v", got != nil, tt.want)
            }
        })
    }
}

// 集成测试(需要真实环境)
func TestClient_Search_Integration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test in short mode")
    }
    
    // 测试逻辑...
}

辅助函数:

// 测试辅助函数以小写字母开头(不导出)
func setupTestClient(t *testing.T) *Client {
    t.Helper()
    
    cfg := &Config{
        Endpoint: "http://localhost:9200",
    }
    client, err := NewClient(cfg)
    if err != nil {
        t.Fatalf("failed to create test client: %v", err)
    }
    return client
}

func cleanupTestIndex(t *testing.T, client *Client, indexName string) {
    t.Helper()
    
    if err := client.DeleteIndex(context.Background(), indexName); err != nil {
        t.Logf("cleanup failed: %v", err)
    }
}

项目阶段考虑

当前阶段(实验期)

允许的简化:

  1. 配置管理:可以使用全局变量
// config/config.go
package config

var (
    // Global configuration (acceptable in experimental phase)
    Endpoint  string
    AccessKey string
    SecretKey string
    Region    string
)

func Init() {
    // Simple initialization from environment variables
    Endpoint = os.Getenv("ES_ENDPOINT")
    AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
    SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
    Region = os.Getenv("AWS_REGION")
}
  1. 日志系统:基础的 log 包使用
// utils/logger.go
package utils

import "log"

var (
    // Global logger (acceptable in experimental phase)
    Logger = log.New(os.Stdout, "[ES-Demo] ", log.LstdFlags)
)

func Info(msg string, args ...any) {
    Logger.Printf("INFO: "+msg, args...)
}

func Error(msg string, args ...any) {
    Logger.Printf("ERROR: "+msg, args...)
}

但必须保持:

  • 代码的可读性和可维护性
  • 清晰的职责划分
  • 完整的错误处理
  • 充分的注释

代码质量检查

使用 Linter 进行自动化检查

本项目使用 golangci-lint 进行代码质量检查。在生成或修改代码后,必须确保代码通过 linter 检查。

配置文件: .golangci.yml

运行 Linter

# 检查所有代码
golangci-lint run ./...

# 自动修复部分问题
golangci-lint run --fix ./...

处理 Linter 输出:

  1. 认真阅读每一条 linter 警告和错误
  2. 理解问题的根本原因
  3. 按照 Go 最佳实践修复问题
  4. 不要通过禁用 linter 规则来"修复"问题(除非有充分理由)
  5. 如需禁用某个规则,必须在配置文件中注释说明原因

常见 Linter 问题及解决方案:

  • errcheck: 错误未处理 → 添加错误处理逻辑
  • gofmt: 格式不正确 → 运行 go fmt ./...
  • govet: 可疑构造 → 重构代码
  • ineffassign: 无效赋值 → 移除或修正赋值
  • staticcheck: 静态分析问题 → 按建议修改

集成到开发流程:

  • 在提交代码前,运行测试流水线(包含 linter 检查)
  • 确保 linter 零警告、零错误
  • CI/CD 流水线应包含 linter 检查步骤

常见模式和示例

客户端初始化模式

// Config holds the configuration for the OpenSearch client.
type Config struct {
    Endpoint  string
    Region    string
    AccessKey string
    SecretKey string
    Timeout   time.Duration
}

// Client represents an OpenSearch client.
type Client struct {
    config *Config
    client *opensearch.Client
}

// NewClient creates a new OpenSearch client with the given configuration.
func NewClient(cfg *Config) (*Client, error) {
    if cfg == nil {
        return nil, fmt.Errorf("config cannot be nil")
    }
    
    if err := validateConfig(cfg); err != nil {
        return nil, fmt.Errorf("invalid config: %w", err)
    }
    
    // Initialize OpenSearch client
    osClient, err := opensearch.NewClient(opensearch.Config{
        Addresses: []string{cfg.Endpoint},
        // ... other settings
    })
    if err != nil {
        return nil, fmt.Errorf("failed to create opensearch client: %w", err)
    }
    
    return &Client{
        config: cfg,
        client: osClient,
    }, nil
}

func validateConfig(cfg *Config) error {
    if cfg.Endpoint == "" {
        return errors.New("endpoint is required")
    }
    if cfg.Region == "" {
        return errors.New("region is required")
    }
    return nil
}

操作方法模式

// SearchRequest represents a search query request.
type SearchRequest struct {
    Index string
    Query map[string]any
    Size  int
    From  int
}

// SearchResult represents the response from a search operation.
type SearchResult struct {
    Hits  []Document
    Total int64
    Took  int64
}

// Search executes a search query against the specified index.
func (c *Client) Search(ctx context.Context, req *SearchRequest) (*SearchResult, error) {
    if req == nil {
        return nil, errors.New("search request cannot be nil")
    }
    
    if req.Index == "" {
        return nil, errors.New("index name is required")
    }
    
    // Build and execute search
    // ...
    
    return result, nil
}

Context 使用模式

// 所有可能阻塞或需要超时控制的操作都应接受 context
func (c *Client) CreateIndex(ctx context.Context, name string, settings map[string]any) error {
    // 使用 context 控制超时
    req, err := http.NewRequestWithContext(ctx, "PUT", c.buildURL(name), body)
    if err != nil {
        return fmt.Errorf("failed to create request: %w", err)
    }
    
    // ...
}

// 调用示例
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := client.CreateIndex(ctx, "my-index", settings)

版本和依赖管理

依赖选择原则:

  1. 优先使用最新稳定版本
  2. 使用官方推荐的库(如 opensearch-go
  3. 定期更新依赖(使用 go get -ugo mod tidy

更新依赖时检查:

# 查看可用更新
go list -u -m all

# 更新特定依赖
go get -u github.com/opensearch-project/opensearch-go/v2@latest

# 整理依赖
go mod tidy

测试流水线

流水线脚本要求

项目必须包含自动化测试流水线脚本,用于执行完整的代码质量检查流程。

脚本命名规范:

  • Windows (PowerShell): test.ps1
  • Linux/Mac (Bash): test.sh

流水线阶段(按顺序执行):

  1. Linting - 代码质量检查

    golangci-lint run ./...
    
  2. Build - 编译验证

    # 编译但不保留二进制文件
    go build -o /dev/null .      # Linux/Mac
    go build -o NUL .            # Windows
    
  3. Test - 运行测试

    go test -v -short -coverprofile=coverage.out ./...
    
  4. Cleanup - 清理构建产物

    # 删除编译输出、覆盖率文件等
    rm -f *.exe *.test coverage.out coverage.html
    

流水线脚本检查

在最终测试前AI Agent 必须检查:

  1. 当前平台是否存在对应的流水线脚本
  2. 如果不存在但有其他平台的脚本,则根据当前平台创建对应版本

平台检测和脚本生成逻辑:

// 伪代码示例
if platform == "Windows" {
    if !exists("test.ps1") {
        if exists("test.sh") {
            // 将 test.sh 翻译为 test.ps1
            createWindowsScript()
        } else {
            // 创建新的 Windows 脚本
            createDefaultWindowsScript()
        }
    }
} else if platform == "Linux" || platform == "Mac" {
    if !exists("test.sh") {
        if exists("test.ps1") {
            // 将 test.ps1 翻译为 test.sh
            createUnixScript()
        } else {
            // 创建新的 Unix 脚本
            createDefaultUnixScript()
        }
    }
}

PowerShell 脚本示例 (test.ps1)

#!/usr/bin/env pwsh
# 测试流水线脚本 - Windows PowerShell

param(
    [switch]$SkipLint,
    [switch]$SkipBuild,
    [switch]$SkipTest,
    [switch]$Verbose
)

$ErrorActionPreference = "Stop"

# 1. Linting
if (-not $SkipLint) {
    Write-Host "=== Linting ===" -ForegroundColor Yellow
    golangci-lint run ./...
    if ($LASTEXITCODE -ne 0) { exit 1 }
}

# 2. Build
if (-not $SkipBuild) {
    Write-Host "=== Build ===" -ForegroundColor Yellow
    go build -o NUL .
    if ($LASTEXITCODE -ne 0) { exit 1 }
}

# 3. Test
if (-not $SkipTest) {
    Write-Host "=== Test ===" -ForegroundColor Yellow
    go test -v -short -coverprofile=coverage.out ./...
    if ($LASTEXITCODE -ne 0) { exit 1 }
    go tool cover -func=coverage.out | Select-String "total:"
}

# 4. Cleanup
Write-Host "=== Cleanup ===" -ForegroundColor Yellow
Remove-Item -Path "*.exe","*.test","coverage.out","coverage.html" -ErrorAction SilentlyContinue

Write-Host "✅ Pipeline PASSED" -ForegroundColor Green

Bash 脚本示例 (test.sh)

#!/usr/bin/env bash
# 测试流水线脚本 - Linux/Mac Bash

set -euo pipefail

SKIP_LINT=false
SKIP_BUILD=false
SKIP_TEST=false
VERBOSE=false

# 解析参数
while [[ $# -gt 0 ]]; do
    case $1 in
        --skip-lint) SKIP_LINT=true; shift ;;
        --skip-build) SKIP_BUILD=true; shift ;;
        --skip-test) SKIP_TEST=true; shift ;;
        --verbose) VERBOSE=true; shift ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

# 1. Linting
if [ "$SKIP_LINT" = false ]; then
    echo "=== Linting ==="
    golangci-lint run ./...
fi

# 2. Build
if [ "$SKIP_BUILD" = false ]; then
    echo "=== Build ==="
    go build -o /dev/null .
fi

# 3. Test
if [ "$SKIP_TEST" = false ]; then
    echo "=== Test ==="
    go test -v -short -race -coverprofile=coverage.out ./...
    go tool cover -func=coverage.out | grep total:
fi

# 4. Cleanup
echo "=== Cleanup ==="
rm -f *.exe *.test coverage.out coverage.html

echo "✅ Pipeline PASSED"

使用流水线

日常开发:

# Windows
powershell -ExecutionPolicy Bypass -File .\test.ps1

# Linux/Mac
chmod +x test.sh
./test.sh

跳过特定阶段:

# Windows - 跳过 linting
powershell -ExecutionPolicy Bypass -File .\test.ps1 -SkipLint

# Linux/Mac - 跳过 build
./test.sh --skip-build

CI/CD 集成:

# GitHub Actions 示例
- name: Run test pipeline
  run: |
    if [ "$RUNNER_OS" == "Windows" ]; then
      pwsh -File test.ps1
    else
      ./test.sh
    fi

流水线最佳实践

  1. 构建产物管理:

    • 编译时不保留二进制文件(输出到 /dev/nullNUL
    • 测试后自动清理所有临时文件
    • .gitignore 中排除所有构建产物
  2. 失败快速原则:

    • 任何阶段失败立即终止流水线
    • 返回非零退出码
    • 提供清晰的错误信息
  3. 可配置性:

    • 提供跳过特定阶段的选项
    • 支持详细输出模式
    • 允许自定义超时设置
  4. 跨平台一致性:

    • 确保 Windows 和 Linux/Mac 脚本功能一致
    • 使用等效的命令和选项
    • 测试结果应该相同

文档要求

代码文档:

  • 所有导出的标识符必须有文档注释
  • 复杂的内部逻辑添加解释性注释
  • 注释应解释"为什么"而不仅仅是"是什么"

README 维护:

  • 添加新功能时更新功能列表
  • 添加示例代码展示用法
  • 更新配置说明

示例代码:

  • examples/ 目录提供可运行的示例
  • 示例代码必须可编译和运行
  • 包含必要的错误处理

AI Agent 工作流程TDD 原则)

当收到开发任务时,必须严格遵循 TDD测试驱动开发流程

1. Red红灯阶段- 编写失败的测试

理解需求:

  • 明确要实现的功能和预期行为
  • 确定输入、输出和边界条件
  • 识别可能的错误场景

设计 API

  • 定义函数签名和类型
  • 设计清晰的接口
  • 确定错误返回值

编写测试(先于实现):

// 示例:先写测试
func TestNewClient(t *testing.T) {
    tests := []struct {
        name    string
        config  *Config
        wantErr error
    }{
        {
            name: "valid config",
            config: &Config{Endpoint: "https://example.com"},
            wantErr: nil,
        },
        {
            name:    "nil config",
            config:  nil,
            wantErr: ErrInvalidConfig,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := NewClient(tt.config)
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("NewClient() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

验证测试失败:

  • 运行测试,确认测试失败(因为功能还未实现)
  • 确保测试失败的原因是正确的

2. Green绿灯阶段- 编写最小实现

实现功能:

  • 编写最少的代码让测试通过
  • 遵循本文档的所有规范
  • 添加完整的文档注释
  • 实现完整的错误处理
// 示例:实现功能
func NewClient(cfg *Config) (*Client, error) {
    if cfg == nil {
        return nil, ErrInvalidConfig
    }
    
    // 最小实现
    return &Client{config: cfg}, nil
}

运行测试:

  • 确保所有测试通过
  • 检查测试覆盖率

3. Refactor重构阶段- 优化代码

代码重构:

  • 消除重复代码
  • 改进命名和结构
  • 优化性能(如需要)
  • 确保代码可读性

运行 Linter

golangci-lint run ./...

修复所有 Linter 问题

再次运行测试:

  • 确保重构后测试仍然通过
  • 验证代码行为未改变

4. 验证和文档

运行测试流水线:

# Windows
powershell -ExecutionPolicy Bypass -File .\test.ps1

# Linux/Mac
./test.sh

更新文档:

  • 更新模块 README如需要
  • 更新项目 README如需要
  • 添加使用示例(如需要)

TDD 循环总结

┌─────────────────────────────────────┐
│  1. 编写失败的测试 (Red)            │
│     - 定义预期行为                   │
│     - 验证测试确实失败               │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│  2. 编写最小实现 (Green)             │
│     - 让测试通过                     │
│     - 不过度设计                     │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│  3. 重构优化 (Refactor)              │
│     - 改进代码质量                   │
│     - 运行 Linter                    │
│     - 确保测试仍然通过               │
└──────────────┬──────────────────────┘
               ↓
         ┌─────────┐
         │ 下一功能 │
         └─────────┘

重要提醒:

  • 不要先写实现再补测试
  • 必须先写测试,然后写实现
  • 每次只关注一个小功能
  • 频繁运行测试,保持快速反馈

特殊指令

生成新模块时

  1. 创建包目录和主文件
  2. 创建对应的测试文件
  3. 在 README 中更新项目结构
  4. 在功能列表中标记进度

重构代码时

  1. 确保向后兼容(除非明确要求破坏性变更)
  2. 更新所有相关测试
  3. 更新文档和注释
  4. 运行完整测试套件

修复 Bug 时

  1. 先编写复现 bug 的测试
  2. 修复代码
  3. 确保测试通过
  4. 添加注释说明修复内容

禁止事项

绝对不允许:

  • 忽略错误(使用 _ 丢弃错误)
  • 导出的标识符没有注释
  • 使用 panic 处理常规错误(除非确实是不可恢复的错误)
  • 在库代码中使用 os.Exit
  • 硬编码配置值(应使用配置文件或环境变量)
  • 使用全局可变状态(除了当前阶段允许的配置和日志)

应当避免:

  • ⚠️ 过长的函数(超过 50 行考虑拆分)
  • ⚠️ 过深的嵌套(超过 3 层考虑重构)
  • ⚠️ 重复代码(提取为函数)
  • ⚠️ 神秘的数字和字符串(使用命名常量)

参考资源


总结: 作为 AI Agent你的目标是生成生产级别的 Go 代码,遵循所有最佳实践,为将来转化为可复用库打下坚实基础。当有疑问时,参考 Go 标准库的实现方式。