24 KiB
applyTo
| applyTo |
|---|
| **/*.go |
AI Agent 开发指令
本文档为 AI Agent(GitHub 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 标准库风格: 参考标准库的代码风格和设计模式
具体要求:
// ✅ 正确:导出的函数必须有文档注释,以函数名开头
// 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. 命名规范
包名:
- 小写、简短、单数形式
- 避免使用下划线或驼峰命名
- 示例:
client、search、document(不是opensearch_client)
接口名:
- 单方法接口以
-er结尾:Reader、Writer、Searcher - 多方法接口使用描述性名称:
IndexManager、DocumentService
变量和函数名:
- 驼峰命名(camelCase 或 PascalCase)
- 缩写词保持一致大写:
URL、HTTP、ID、JSON(不是Url、Http、Id、Json) - 示例:
clientID、httpClient、parseURL
常量:
// ✅ 正确:使用驼峰命名
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) - 测试函数:
TestNewClient、TestClient_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)
}
}
项目阶段考虑
当前阶段(实验期)
允许的简化:
- 配置管理:可以使用全局变量
// 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")
}
- 日志系统:基础的 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 输出:
- 认真阅读每一条 linter 警告和错误
- 理解问题的根本原因
- 按照 Go 最佳实践修复问题
- 不要通过禁用 linter 规则来"修复"问题(除非有充分理由)
- 如需禁用某个规则,必须在配置文件中注释说明原因
常见 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)
版本和依赖管理
依赖选择原则:
- 优先使用最新稳定版本
- 使用官方推荐的库(如 opensearch-go)
- 定期更新依赖(使用
go get -u和go 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
流水线阶段(按顺序执行):
-
Linting - 代码质量检查
golangci-lint run ./... -
Build - 编译验证
# 编译但不保留二进制文件 go build -o /dev/null . # Linux/Mac go build -o NUL . # Windows -
Test - 运行测试
go test -v -short -coverprofile=coverage.out ./... -
Cleanup - 清理构建产物
# 删除编译输出、覆盖率文件等 rm -f *.exe *.test coverage.out coverage.html
流水线脚本检查
在最终测试前,AI Agent 必须检查:
- 当前平台是否存在对应的流水线脚本
- 如果不存在但有其他平台的脚本,则根据当前平台创建对应版本
平台检测和脚本生成逻辑:
// 伪代码示例
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
流水线最佳实践
-
构建产物管理:
- 编译时不保留二进制文件(输出到
/dev/null或NUL) - 测试后自动清理所有临时文件
- 在
.gitignore中排除所有构建产物
- 编译时不保留二进制文件(输出到
-
失败快速原则:
- 任何阶段失败立即终止流水线
- 返回非零退出码
- 提供清晰的错误信息
-
可配置性:
- 提供跳过特定阶段的选项
- 支持详细输出模式
- 允许自定义超时设置
-
跨平台一致性:
- 确保 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 │
│ - 确保测试仍然通过 │
└──────────────┬──────────────────────┘
↓
┌─────────┐
│ 下一功能 │
└─────────┘
重要提醒:
- ❌ 不要先写实现再补测试
- ✅ 必须先写测试,然后写实现
- ✅ 每次只关注一个小功能
- ✅ 频繁运行测试,保持快速反馈
特殊指令
生成新模块时
- 创建包目录和主文件
- 创建对应的测试文件
- 在 README 中更新项目结构
- 在功能列表中标记进度
重构代码时
- 确保向后兼容(除非明确要求破坏性变更)
- 更新所有相关测试
- 更新文档和注释
- 运行完整测试套件
修复 Bug 时
- 先编写复现 bug 的测试
- 修复代码
- 确保测试通过
- 添加注释说明修复内容
禁止事项
绝对不允许:
- ❌ 忽略错误(使用
_丢弃错误) - ❌ 导出的标识符没有注释
- ❌ 使用
panic处理常规错误(除非确实是不可恢复的错误) - ❌ 在库代码中使用
os.Exit - ❌ 硬编码配置值(应使用配置文件或环境变量)
- ❌ 使用全局可变状态(除了当前阶段允许的配置和日志)
应当避免:
- ⚠️ 过长的函数(超过 50 行考虑拆分)
- ⚠️ 过深的嵌套(超过 3 层考虑重构)
- ⚠️ 重复代码(提取为函数)
- ⚠️ 神秘的数字和字符串(使用命名常量)
参考资源
总结: 作为 AI Agent,你的目标是生成生产级别的 Go 代码,遵循所有最佳实践,为将来转化为可复用库打下坚实基础。当有疑问时,参考 Go 标准库的实现方式。