feat: opensearch客户端初始化 feat: 索引模板接口 ai: 开发准则 chore: TDD流水线脚本

This commit is contained in:
mouseleee
2025-11-16 22:17:16 +08:00
commit da3883205c
18 changed files with 2707 additions and 0 deletions

View File

@@ -0,0 +1,975 @@
---
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 标准库的实现方式。