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

976 lines
24 KiB
Markdown
Raw Permalink 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.

---
applyTo: '**/*.go'
---
# AI Agent 开发指令
本文档为 AI AgentGitHub Copilot提供开发本项目时的详细指令和指南。
## 项目背景
这是一个用于测试和实验 AWS OpenSearch Service 的 Go 项目。项目目标是创建生产级别的代码,并为未来转化为可复用的依赖库做准备。
## 核心原则
### 1. 代码质量要求(最高优先级)
**必须遵循 Go 官方最佳实践:**
- **Effective Go**: https://go.dev/doc/effective_go
- **Go Code Review Comments**: https://github.com/golang/go/wiki/CodeReviewComments
- **Go 标准库风格**: 参考标准库的代码风格和设计模式
**具体要求:**
```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) {
// ...
}
```
```go
// ✅ 正确:错误处理不能忽略
result, err := client.Search(ctx, query)
if err != nil {
return nil, fmt.Errorf("search failed: %w", err)
}
// ❌ 错误:忽略错误
result, _ := client.Search(ctx, query)
```
```go
// ✅ 正确:使用有意义的变量名
var maxRetryCount = 3
var connectionTimeout = 30 * time.Second
// ❌ 错误:使用无意义的缩写
var mrc = 3
var ct = 30 * time.Second
```
### 2. 命名规范
**包名:**
- 小写、简短、单数形式
- 避免使用下划线或驼峰命名
- 示例:`client``search``document`(不是 `opensearch_client`
**接口名:**
- 单方法接口以 `-er` 结尾:`Reader``Writer``Searcher`
- 多方法接口使用描述性名称:`IndexManager``DocumentService`
**变量和函数名:**
- 驼峰命名camelCase 或 PascalCase
- 缩写词保持一致大写:`URL``HTTP``ID``JSON`(不是 `Url``Http``Id``Json`
- 示例:`clientID``httpClient``parseURL`
**常量:**
```go
// ✅ 正确:使用驼峰命名
const DefaultTimeout = 30 * time.Second
const MaxRetries = 3
// 或者使用全大写(枚举类型)
const (
StatusPending = "PENDING"
StatusComplete = "COMPLETE"
)
```
### 3. 错误处理规范
**基本错误处理:**
```go
// ✅ 正确:包装错误,提供上下文信息
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() 等清理操作,**不能**完全忽略错误:
```go
// ✅ 正确:检查并记录 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. 代码结构规范
**文件组织:**
```go
// 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 {
// ...
}
```
**接口设计:**
```go
// ✅ 正确:接口定义在使用方,而非实现方
// 在 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`
- 测试函数:`TestNewClient``TestClient_Search`
**测试结构:**
```go
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")
}
// 测试逻辑...
}
```
**辅助函数:**
```go
// 测试辅助函数以小写字母开头(不导出)
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. **配置管理**:可以使用全局变量
```go
// 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")
}
```
2. **日志系统**:基础的 log 包使用
```go
// 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**
```bash
# 检查所有代码
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 检查步骤
## 常见模式和示例
### 客户端初始化模式
```go
// 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
}
```
### 操作方法模式
```go
// 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 使用模式
```go
// 所有可能阻塞或需要超时控制的操作都应接受 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 -u``go mod tidy`
**更新依赖时检查:**
```bash
# 查看可用更新
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** - 代码质量检查
```bash
golangci-lint run ./...
```
2. **Build** - 编译验证
```bash
# 编译但不保留二进制文件
go build -o /dev/null . # Linux/Mac
go build -o NUL . # Windows
```
3. **Test** - 运行测试
```bash
go test -v -short -coverprofile=coverage.out ./...
```
4. **Cleanup** - 清理构建产物
```bash
# 删除编译输出、覆盖率文件等
rm -f *.exe *.test coverage.out coverage.html
```
### 流水线脚本检查
**在最终测试前AI Agent 必须检查:**
1. 当前平台是否存在对应的流水线脚本
2. 如果不存在但有其他平台的脚本,则根据当前平台创建对应版本
**平台检测和脚本生成逻辑:**
```go
// 伪代码示例
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)
```powershell
#!/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)
```bash
#!/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"
```
### 使用流水线
**日常开发:**
```bash
# Windows
powershell -ExecutionPolicy Bypass -File .\test.ps1
# Linux/Mac
chmod +x test.sh
./test.sh
```
**跳过特定阶段:**
```bash
# Windows - 跳过 linting
powershell -ExecutionPolicy Bypass -File .\test.ps1 -SkipLint
# Linux/Mac - 跳过 build
./test.sh --skip-build
```
**CI/CD 集成:**
```yaml
# GitHub Actions 示例
- name: Run test pipeline
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
pwsh -File test.ps1
else
./test.sh
fi
```
### 流水线最佳实践
1. **构建产物管理:**
- 编译时不保留二进制文件(输出到 `/dev/null` 或 `NUL`
- 测试后自动清理所有临时文件
- 在 `.gitignore` 中排除所有构建产物
2. **失败快速原则:**
- 任何阶段失败立即终止流水线
- 返回非零退出码
- 提供清晰的错误信息
3. **可配置性:**
- 提供跳过特定阶段的选项
- 支持详细输出模式
- 允许自定义超时设置
4. **跨平台一致性:**
- 确保 Windows 和 Linux/Mac 脚本功能一致
- 使用等效的命令和选项
- 测试结果应该相同
## 文档要求
**代码文档:**
- 所有导出的标识符必须有文档注释
- 复杂的内部逻辑添加解释性注释
- 注释应解释"为什么"而不仅仅是"是什么"
**README 维护:**
- 添加新功能时更新功能列表
- 添加示例代码展示用法
- 更新配置说明
**示例代码:**
- 在 `examples/` 目录提供可运行的示例
- 示例代码必须可编译和运行
- 包含必要的错误处理
## AI Agent 工作流程TDD 原则)
当收到开发任务时,**必须严格遵循 TDD测试驱动开发流程**
### 1. Red红灯阶段- 编写失败的测试
**理解需求:**
- 明确要实现的功能和预期行为
- 确定输入、输出和边界条件
- 识别可能的错误场景
**设计 API**
- 定义函数签名和类型
- 设计清晰的接口
- 确定错误返回值
**编写测试(先于实现):**
```go
// 示例:先写测试
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绿灯阶段- 编写最小实现
**实现功能:**
- 编写最少的代码让测试通过
- 遵循本文档的所有规范
- 添加完整的文档注释
- 实现完整的错误处理
```go
// 示例:实现功能
func NewClient(cfg *Config) (*Client, error) {
if cfg == nil {
return nil, ErrInvalidConfig
}
// 最小实现
return &Client{config: cfg}, nil
}
```
**运行测试:**
- 确保所有测试通过
- 检查测试覆盖率
### 3. Refactor重构阶段- 优化代码
**代码重构:**
- 消除重复代码
- 改进命名和结构
- 优化性能(如需要)
- 确保代码可读性
**运行 Linter**
```bash
golangci-lint run ./...
```
**修复所有 Linter 问题**
**再次运行测试:**
- 确保重构后测试仍然通过
- 验证代码行为未改变
### 4. 验证和文档
**运行测试流水线:**
```bash
# 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 层考虑重构)
- ⚠️ 重复代码(提取为函数)
- ⚠️ 神秘的数字和字符串(使用命名常量)
## 参考资源
- [Effective Go](https://go.dev/doc/effective_go)
- [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
- [Go 标准库文档](https://pkg.go.dev/std)
- [OpenSearch Go Client](https://github.com/opensearch-project/opensearch-go)
- [AWS SDK for Go v2](https://aws.github.io/aws-sdk-go-v2/)
---
**总结**: 作为 AI Agent你的目标是生成生产级别的 Go 代码,遵循所有最佳实践,为将来转化为可复用库打下坚实基础。当有疑问时,参考 Go 标准库的实现方式。