From 13ec81974de5cdfc1e7373b9123f950574331774 Mon Sep 17 00:00:00 2001 From: qloog Date: Sat, 28 Feb 2026 16:23:24 +0800 Subject: [PATCH 1/4] feat: add claude onboard skill --- .claude/skills/eagle-onboarding/SKILL.md | 1144 +++++++++++++++++ .../skills/eagle-onboarding/evals/evals.json | 23 + 2 files changed, 1167 insertions(+) create mode 100644 .claude/skills/eagle-onboarding/SKILL.md create mode 100644 .claude/skills/eagle-onboarding/evals/evals.json diff --git a/.claude/skills/eagle-onboarding/SKILL.md b/.claude/skills/eagle-onboarding/SKILL.md new file mode 100644 index 0000000..dd6360e --- /dev/null +++ b/.claude/skills/eagle-onboarding/SKILL.md @@ -0,0 +1,1144 @@ +--- +name: eagle-onboarding +description: Eagle 框架 Go 微服务项目新手入门指导。适用于刚加入团队的开发者需要了解项目架构、开发命令、工作流程时使用。当用户询问"如何开始"、"怎么添加新功能"、"项目结构是什么"、"开发流程"、"如何添加 API"、"修改数据模型"、"Wire 怎么用"、"gRPC 如何配置"等相关问题时触发。确保在新人提出项目相关问题、需要架构说明或开发指导时主动使用此 skill。 +--- + +# Eagle 框架新手入门指南 + +欢迎加入 Eagle 微服务项目!这份指南将帮助你快速了解项目结构、掌握开发流程,并学会如何添加新功能。 + +## 🏗️ 项目架构概览 + +本项目基于 **Clean Architecture(整洁架构)** 设计,使用 Eagle 框架构建。核心分为三层: + +``` +┌─────────────────────────────────────────┐ +│ Service Layer (业务逻辑层) │ +│ • 处理业务逻辑 │ +│ • 协议转换 (gRPC/HTTP) │ +│ • 输入输出验证 │ +└─────────────┬───────────────────────────┘ + │ +┌─────────────▼───────────────────────────┐ +│ Repository Layer (仓储抽象层) │ +│ • 统一数据访问接口 │ +│ • 缓存策略管理 │ +│ • 数据聚合逻辑 │ +└─────────────┬───────────────────────────┘ + │ +┌─────────────▼───────────────────────────┐ +│ DAL Layer (数据访问层) │ +│ ├─ DB: 数据库操作 (GORM) │ +│ ├─ Cache: Redis 缓存 │ +│ └─ RPC: 外部服务调用 │ +└─────────────────────────────────────────┘ +``` + +### 目录结构 + +``` +eagle-layout/ +├── api/ # Proto 文件定义 +│ ├── user/v1/ # 用户服务 API 定义 +│ └── helloworld/ # 示例服务 +├── cmd/ # 应用入口 +│ ├── server/ # HTTP/gRPC 服务器 +│ │ ├── main.go # 主程序入口 +│ │ ├── wire.go # Wire 依赖注入定义 +│ │ └── wire_gen.go # Wire 生成的代码 +│ └── consumer/ # 后台任务消费者 +├── config/ # 配置文件 +│ ├── dev/ # 开发环境配置 +│ ├── test/ # 测试环境配置 +│ └── prod/ # 生产环境配置 +├── internal/ # 内部代码(不对外暴露) +│ ├── service/ # Service 层:业务逻辑 +│ │ ├── user_svc.go # 业务逻辑实现 +│ │ └── user_grpc.go # gRPC 协议转换 +│ ├── repository/ # Repository 层:数据访问抽象 +│ │ └── user_repo.go # 仓储接口和实现 +│ ├── dal/ # DAL 层:底层数据访问 +│ │ ├── db/ # 数据库相关 +│ │ │ ├── dao/ # GORM Gen 生成的 DAO +│ │ │ └── model/ # 数据模型 +│ │ ├── cache/ # Redis 缓存 +│ │ └── rpc/ # 外部服务调用 +│ ├── server/ # 服务器配置 +│ ├── types/ # 内部类型定义 +│ └── ecode/ # 错误码定义 +└── Makefile # 开发命令集合 +``` + +## 🔧 常用开发命令 + +所有命令都定义在 `Makefile` 中,使用 `make ` 执行: + +### 构建与运行 + +```bash +# 运行服务器(包含 Wire 依赖注入) +make run + +# 构建二进制文件到 bin/eagle-service +# 包含版本信息和竞态检测 +make build +``` + +### 代码生成 + +```bash +# 生成 Wire 依赖注入代码 +# 会生成 cmd/server/wire_gen.go +make wire + +# 从 .proto 文件生成 gRPC 和 Protocol Buffer 代码 +# 生成文件到 api/ 目录下对应位置 +make grpc + +# 生成 Protocol Buffer 结构体(带验证) +make proto + +# 生成 GORM 模型文件 +# 使用 cmd/gen/generate.go 配置 +make gorm-gen +``` + +### 测试与质量 + +```bash +# 运行测试(带竞态检测) +make test + +# 代码检查(golangci-lint) +make lint + +# 生成测试覆盖率报告 +make cover + +# 生成并查看 HTML 覆盖率报告 +make view-cover +``` + +### 文档 + +```bash +# 生成 Swagger 文档 +make docs + +# 访问 Swagger UI +# http://localhost:8080/swagger/index.html +``` + +## 🚀 核心技术栈使用说明 + +### 1. Google Wire 依赖注入 + +Wire 是编译时依赖注入工具,避免运行时反射带来的性能损耗。 + +#### Wire 工作原理 + +**定义 Provider(提供者)**: + +`cmd/server/wire.go`: +```go +//go:build wireinject +// +build wireinject + +package main + +import ( + "github.com/google/wire" + "github.com/go-eagle/eagle-layout/internal/server" +) + +// InitApp 定义依赖关系 +func InitApp(cfg *eagle.Config) (*eagle.App, func(), error) { + wire.Build( + server.ServerSet, // Server 层依赖集 + newApp, // App 构造函数 + ) + return &eagle.App{}, nil, nil +} +``` + +**Provider Set 示例**: + +`internal/server/grpc.go`: +```go +// ProviderSet is server providers. +var ProviderSet = wire.NewSet( + NewHTTPServer, + NewGRPCServer, + service.ProviderSet, + repository.ProviderSet, + dal.ProviderSet, +) +``` + +**生成代码**: +```bash +make wire +# 生成 cmd/server/wire_gen.go +``` + +#### 添加新依赖的步骤 + +1. 创建构造函数(返回接口类型): +```go +// internal/service/order_svc.go +func NewOrderService(repo repository.OrderRepo) *OrderService { + return &OrderService{repo: repo} +} +``` + +2. 添加到 ProviderSet: +```go +// internal/service/service.go +var ProviderSet = wire.NewSet( + NewUserService, + NewOrderService, // 新增 +) +``` + +3. 重新生成 Wire 代码: +```bash +make wire +``` + +### 2. gRPC 和 Protocol Buffers + +#### Proto 文件定义 + +`api/user/v1/user.proto`: +```protobuf +syntax = "proto3"; + +package user.v1; + +import "validate/validate.proto"; +import "google/api/annotations.proto"; + +option go_package = "github.com/go-eagle/eagle-layout/api/user/v1;v1"; + +// 用户服务 +service UserService { + // 创建用户 + rpc CreateUser(CreateUserRequest) returns(CreateUserReply) { + option (google.api.http) = { + post: "/v1/users/" + body: "*" + }; + } + + // 获取用户 + rpc GetUser(GetUserRequest) returns (GetUserReply) { + option (google.api.http) = { + get: "/v1/users/{id}" + }; + } +} + +// 请求消息(带验证) +message CreateUserRequest { + string username = 1 [(validate.rules).string.min_len = 6]; + string email = 2 [(validate.rules).string.email = true]; + string password = 3 [(validate.rules).string.min_len = 6]; +} + +// 响应消息 +message CreateUserReply { + int64 id = 1; + string username = 2; + string email = 3; +} +``` + +#### 生成代码 + +```bash +# 从 proto 文件生成 Go 代码 +make grpc + +# 生成的文件: +# api/user/v1/user.pb.go - Protocol Buffer 定义 +# api/user/v1/user_grpc.pb.go - gRPC 服务代码 +# api/user/v1/user.pb.validate.go - 验证代码 +# api/user/v1/user_http.pb.go - HTTP 网关代码 +``` + +#### 实现 gRPC 服务 + +`internal/service/user_grpc.go`: +```go +package service + +import ( + "context" + pb "github.com/go-eagle/eagle-layout/api/user/v1" + "github.com/go-eagle/eagle-layout/internal/types" +) + +// 确保实现了 gRPC 接口 +var _ pb.UserServiceServer = (*UserService)(nil) + +// CreateUser implements gRPC CreateUser method +func (s *UserService) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserReply, error) { + // 1. 协议转换:gRPC -> 内部类型 + input := types.CreateUserInput{ + Username: req.Username, + Email: req.Email, + Password: req.Password, + } + + // 2. 调用业务逻辑 + output, err := s.CreateUser(ctx, input) + if err != nil { + return nil, err + } + + // 3. 协议转换:内部类型 -> gRPC + return &pb.CreateUserReply{ + Id: output.ID, + Username: output.Username, + Email: output.Email, + }, nil +} +``` + +### 3. GORM 数据库操作 + +本项目使用 **GORM Gen** 生成类型安全的数据库操作代码。 + +#### 数据模型定义 + +`internal/dal/db/model/user.go`: +```go +package model + +// UserInfoModel 用户信息表 +type UserInfoModel struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement"` + Username string `gorm:"column:username;type:varchar(50);uniqueIndex;not null"` + Email string `gorm:"column:email;type:varchar(100);uniqueIndex;not null"` + Password string `gorm:"column:password;type:varchar(255);not null"` + Nickname string `gorm:"column:nickname;type:varchar(50)"` + Avatar string `gorm:"column:avatar;type:varchar(255)"` + Status int32 `gorm:"column:status;default:0"` + CreatedAt int64 `gorm:"column:created_at;autoCreateTime"` + UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime"` +} + +func (UserInfoModel) TableName() string { + return "user_info" +} +``` + +#### 生成 DAO + +`cmd/gen/generate.go`: +```go +package main + +import ( + "gorm.io/gen" + "github.com/go-eagle/eagle-layout/internal/dal/db/model" +) + +func main() { + g := gen.NewGenerator(gen.Config{ + OutPath: "./internal/dal/db/dao", + Mode: gen.WithDefaultQuery | gen.WithQueryInterface, + }) + + // 使用数据库连接 + g.UseDB(db) + + // 生成模型对应的 DAO + g.ApplyBasic(model.UserInfoModel{}) + + // 自定义查询方法 + g.ApplyInterface(func(method gen.Method) {}, model.UserInfoModel{}) + + g.Execute() +} +``` + +运行生成: +```bash +make gorm-gen +# 生成到 internal/dal/db/dao/ +``` + +#### 使用 DAO 查询 + +Repository 层使用生成的 DAO: + +`internal/repository/user_repo.go`: +```go +package repository + +import ( + "context" + "github.com/go-eagle/eagle-layout/internal/dal/db/dao" + "github.com/go-eagle/eagle-layout/internal/dal/db/model" +) + +type userRepo struct { + db *dal.DBClient + cache cache.UserCache +} + +// GetUser 获取单个用户 +func (r *userRepo) GetUser(ctx context.Context, id int64) (*model.UserInfoModel, error) { + // 使用生成的 DAO 查询 + user, err := dao.UserInfoModel.WithContext(ctx). + Where(dao.UserInfoModel.ID.Eq(id)). + First() + if err != nil { + return nil, err + } + return user, nil +} + +// CreateUser 创建用户 +func (r *userRepo) CreateUser(ctx context.Context, data model.UserInfoModel) (int64, error) { + err := dao.UserInfoModel.WithContext(ctx).Create(&data) + if err != nil { + return 0, err + } + return data.ID, nil +} + +// UpdateUser 更新用户 +func (r *userRepo) UpdateUser(ctx context.Context, id int64, data model.UserInfoModel) error { + _, err := dao.UserInfoModel.WithContext(ctx). + Where(dao.UserInfoModel.ID.Eq(id)). + Updates(data) + return err +} +``` + +#### 缓存集成 + +Repository 层集成多级缓存(本地缓存 + Redis): + +`internal/repository/user_repo.go`: +```go +func (r *userRepo) GetUser(ctx context.Context, id int64) (*model.UserInfoModel, error) { + // 1. 尝试本地缓存 + var ret *model.UserInfoModel + err := r.localCache.Get(ctx, cast.ToString(id), &ret) + if err == nil && ret != nil && ret.ID > 0 { + return ret, nil + } + + // 2. 尝试 Redis 缓存 + ret, err = r.cache.GetUserCache(ctx, id) + if err == nil && ret != nil && ret.ID > 0 { + return ret, nil + } + + // 3. 查询数据库(使用 singleflight 防止缓存击穿) + val, err, _ := r.sg.Do("sg:user:"+cast.ToString(id), func() (interface{}, error) { + data, err := dao.UserInfoModel.WithContext(ctx). + Where(dao.UserInfoModel.ID.Eq(id)). + First() + if err != nil { + return nil, err + } + + // 4. 写入缓存 + _ = r.cache.SetUserCache(ctx, id, data, 5*time.Minute) + _ = r.localCache.Set(ctx, cast.ToString(id), data, 2*time.Minute) + + return data, nil + }) + + if err != nil { + return nil, err + } + + return val.(*model.UserInfoModel), nil +} +``` + +## 📝 完整开发工作流程 + +### 场景 1: 添加新的 API 接口 + +假设你要添加一个"获取用户列表"的接口。 + +#### 步骤 1: 定义 Proto + +编辑 `api/user/v1/user.proto`: + +```protobuf +service UserService { + // 新增:获取用户列表 + rpc ListUsers(ListUsersRequest) returns (ListUsersReply) { + option (google.api.http) = { + get: "/v1/users" + }; + } +} + +message ListUsersRequest { + int32 page = 1 [(validate.rules).int32.gte = 1]; + int32 page_size = 2 [(validate.rules).int32 = {gte: 1, lte: 100}]; +} + +message ListUsersReply { + repeated User users = 1; + int32 total = 2; +} +``` + +#### 步骤 2: 生成 gRPC 代码 + +```bash +make grpc +``` + +#### 步骤 3: 定义内部类型 + +创建 `internal/types/user.go`: + +```go +package types + +type ListUsersInput struct { + Page int32 + PageSize int32 +} + +type ListUsersOutput struct { + Users []*User + Total int32 +} +``` + +#### 步骤 4: 实现 Repository 层 + +在 `internal/repository/user_repo.go` 添加接口方法: + +```go +type UserRepo interface { + // ... 现有方法 + ListUsers(ctx context.Context, page, pageSize int32) ([]*model.UserInfoModel, int64, error) +} + +func (r *userRepo) ListUsers(ctx context.Context, page, pageSize int32) ([]*model.UserInfoModel, int64, error) { + offset := (page - 1) * pageSize + + // 查询列表 + users, err := dao.UserInfoModel.WithContext(ctx). + Limit(int(pageSize)). + Offset(int(offset)). + Order(dao.UserInfoModel.CreatedAt.Desc()). + Find() + if err != nil { + return nil, 0, err + } + + // 查询总数 + total, err := dao.UserInfoModel.WithContext(ctx).Count() + if err != nil { + return nil, 0, err + } + + return users, total, nil +} +``` + +#### 步骤 5: 实现 Service 层业务逻辑 + +在 `internal/service/user_svc.go` 添加: + +```go +func (s *UserService) ListUsers(ctx context.Context, input types.ListUsersInput) (*types.ListUsersOutput, error) { + users, total, err := s.repo.ListUsers(ctx, input.Page, input.PageSize) + if err != nil { + return nil, fmt.Errorf("[UserService] ListUsers error: %w", err) + } + + // 转换为内部类型 + var userList []*types.User + for _, u := range users { + user, err := s.convertUser(u) + if err != nil { + continue + } + userList = append(userList, user) + } + + return &types.ListUsersOutput{ + Users: userList, + Total: int32(total), + }, nil +} +``` + +#### 步骤 6: 实现 gRPC 协议转换 + +在 `internal/service/user_grpc.go` 添加: + +```go +func (s *UserService) ListUsers(ctx context.Context, req *pb.ListUsersRequest) (*pb.ListUsersReply, error) { + // gRPC -> 内部类型 + input := types.ListUsersInput{ + Page: req.Page, + PageSize: req.PageSize, + } + + // 调用业务逻辑 + output, err := s.ListUsers(ctx, input) + if err != nil { + return nil, err + } + + // 内部类型 -> gRPC + var pbUsers []*pb.User + for _, u := range output.Users { + pbUsers = append(pbUsers, &pb.User{ + Id: u.Id, + Username: u.Username, + Email: u.Email, + // ... 其他字段 + }) + } + + return &pb.ListUsersReply{ + Users: pbUsers, + Total: output.Total, + }, nil +} +``` + +#### 步骤 7: 测试 + +```bash +# 运行服务 +make run + +# 测试 HTTP 接口 +curl "http://localhost:8080/v1/users?page=1&page_size=10" + +# 运行单元测试 +make test +``` + +### 场景 2: 修改数据模型 + +假设要给用户表添加"最后登录时间"字段。 + +#### 步骤 1: 修改数据库表结构 + +```sql +ALTER TABLE user_info +ADD COLUMN last_login_at BIGINT DEFAULT 0 COMMENT '最后登录时间'; +``` + +#### 步骤 2: 更新 Model 定义 + +编辑 `internal/dal/db/model/user.go`: + +```go +type UserInfoModel struct { + // ... 现有字段 + LastLoginAt int64 `gorm:"column:last_login_at;default:0"` +} +``` + +#### 步骤 3: 重新生成 DAO + +```bash +make gorm-gen +``` + +#### 步骤 4: 更新 Proto 定义 + +编辑 `api/user/v1/user.proto`: + +```protobuf +message User { + // ... 现有字段 + int64 last_login_at = 14; +} +``` + +```bash +make grpc +``` + +#### 步骤 5: 更新相关业务逻辑 + +在需要的地方更新字段使用: + +```go +// internal/service/user_svc.go +func (s *UserService) Login(ctx context.Context, input types.LoginInput) (*types.LoginOutput, error) { + // ... 登录逻辑 + + // 更新最后登录时间 + err = s.repo.UpdateUser(ctx, user.ID, model.UserInfoModel{ + LastLoginAt: time.Now().Unix(), + }) + + // ... +} +``` + +### 场景 3: 添加新的业务服务 + +假设要添加订单服务(Order Service)。 + +#### 步骤 1: 创建 Proto 定义 + +创建 `api/order/v1/order.proto`: + +```protobuf +syntax = "proto3"; + +package order.v1; + +import "validate/validate.proto"; +import "google/api/annotations.proto"; + +option go_package = "github.com/go-eagle/eagle-layout/api/order/v1;v1"; + +service OrderService { + rpc CreateOrder(CreateOrderRequest) returns (CreateOrderReply) { + option (google.api.http) = { + post: "/v1/orders" + body: "*" + }; + } + + rpc GetOrder(GetOrderRequest) returns (GetOrderReply) { + option (google.api.http) = { + get: "/v1/orders/{id}" + }; + } +} + +message CreateOrderRequest { + int64 user_id = 1 [(validate.rules).int64.gte = 1]; + repeated int64 product_ids = 2; + string address = 3; +} + +message CreateOrderReply { + int64 id = 1; + string order_no = 2; +} + +message GetOrderRequest { + int64 id = 1; +} + +message GetOrderReply { + Order order = 1; +} + +message Order { + int64 id = 1; + string order_no = 2; + int64 user_id = 3; + int64 total_amount = 4; + int32 status = 5; +} +``` + +生成代码: +```bash +make grpc +``` + +#### 步骤 2: 创建数据模型 + +创建 `internal/dal/db/model/order.go`: + +```go +package model + +type OrderModel struct { + ID int64 `gorm:"column:id;primaryKey;autoIncrement"` + OrderNo string `gorm:"column:order_no;type:varchar(50);uniqueIndex;not null"` + UserID int64 `gorm:"column:user_id;index;not null"` + TotalAmount int64 `gorm:"column:total_amount;not null"` + Status int32 `gorm:"column:status;default:0"` + CreatedAt int64 `gorm:"column:created_at;autoCreateTime"` + UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime"` +} + +func (OrderModel) TableName() string { + return "orders" +} +``` + +生成 DAO: +```bash +# 更新 cmd/gen/generate.go,添加 OrderModel +# 然后运行 +make gorm-gen +``` + +#### 步骤 3: 创建 Repository 层 + +创建 `internal/repository/order_repo.go`: + +```go +package repository + +import ( + "context" + "github.com/go-eagle/eagle-layout/internal/dal" + "github.com/go-eagle/eagle-layout/internal/dal/db/dao" + "github.com/go-eagle/eagle-layout/internal/dal/db/model" +) + +type OrderRepo interface { + CreateOrder(ctx context.Context, data model.OrderModel) (int64, error) + GetOrder(ctx context.Context, id int64) (*model.OrderModel, error) +} + +type orderRepo struct { + db *dal.DBClient +} + +func NewOrderRepo(db *dal.DBClient) OrderRepo { + return &orderRepo{db: db} +} + +func (r *orderRepo) CreateOrder(ctx context.Context, data model.OrderModel) (int64, error) { + err := dao.OrderModel.WithContext(ctx).Create(&data) + if err != nil { + return 0, err + } + return data.ID, nil +} + +func (r *orderRepo) GetOrder(ctx context.Context, id int64) (*model.OrderModel, error) { + return dao.OrderModel.WithContext(ctx). + Where(dao.OrderModel.ID.Eq(id)). + First() +} +``` + +更新 `internal/repository/repository.go` 的 ProviderSet: + +```go +var ProviderSet = wire.NewSet( + NewUserRepo, + NewOrderRepo, // 新增 +) +``` + +#### 步骤 4: 创建 Service 层 + +创建 `internal/service/order_svc.go`: + +```go +package service + +import ( + "context" + "fmt" + "time" + "github.com/go-eagle/eagle-layout/internal/repository" + "github.com/go-eagle/eagle-layout/internal/dal/db/model" + "github.com/go-eagle/eagle-layout/internal/types" +) + +type OrderService struct { + repo repository.OrderRepo +} + +func NewOrderService(repo repository.OrderRepo) *OrderService { + return &OrderService{repo: repo} +} + +func (s *OrderService) CreateOrder(ctx context.Context, input types.CreateOrderInput) (*types.CreateOrderOutput, error) { + // 生成订单号 + orderNo := fmt.Sprintf("ORD%d", time.Now().UnixNano()) + + // 创建订单 + order := model.OrderModel{ + OrderNo: orderNo, + UserID: input.UserID, + TotalAmount: input.TotalAmount, + Status: 0, + CreatedAt: time.Now().Unix(), + } + + id, err := s.repo.CreateOrder(ctx, order) + if err != nil { + return nil, fmt.Errorf("[OrderService] CreateOrder error: %w", err) + } + + return &types.CreateOrderOutput{ + ID: id, + OrderNo: orderNo, + }, nil +} +``` + +创建 `internal/service/order_grpc.go`: + +```go +package service + +import ( + "context" + pb "github.com/go-eagle/eagle-layout/api/order/v1" + "github.com/go-eagle/eagle-layout/internal/types" +) + +var _ pb.OrderServiceServer = (*OrderService)(nil) + +func (s *OrderService) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderReply, error) { + input := types.CreateOrderInput{ + UserID: req.UserId, + ProductIDs: req.ProductIds, + TotalAmount: 0, // 实际应计算 + } + + output, err := s.CreateOrder(ctx, input) + if err != nil { + return nil, err + } + + return &pb.CreateOrderReply{ + Id: output.ID, + OrderNo: output.OrderNo, + }, nil +} +``` + +更新 `internal/service/service.go` 的 ProviderSet: + +```go +var ProviderSet = wire.NewSet( + NewUserService, + NewOrderService, // 新增 +) +``` + +#### 步骤 5: 注册 gRPC 服务 + +编辑 `internal/server/grpc.go`: + +```go +package server + +import ( + userV1 "github.com/go-eagle/eagle-layout/api/user/v1" + orderV1 "github.com/go-eagle/eagle-layout/api/order/v1" // 新增 + "github.com/go-eagle/eagle-layout/internal/service" +) + +func NewGRPCServer( + cfg *eagle.Config, + userSvc *service.UserService, + orderSvc *service.OrderService, // 新增参数 +) *grpc.Server { + // ... + + // 注册服务 + userV1.RegisterUserServiceServer(srv, userSvc) + orderV1.RegisterOrderServiceServer(srv, orderSvc) // 新增注册 + + return srv +} +``` + +#### 步骤 6: 重新生成 Wire 代码 + +```bash +make wire +``` + +Wire 会自动分析依赖关系并生成初始化代码: +- `OrderService` 依赖 `OrderRepo` +- `OrderRepo` 依赖 `DBClient` +- 所有依赖会自动注入 + +#### 步骤 7: 测试 + +```bash +# 运行服务 +make run + +# 测试创建订单 +curl -X POST http://localhost:8080/v1/orders \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": 1, + "product_ids": [101, 102], + "address": "上海市浦东新区" + }' + +# 测试获取订单 +curl http://localhost:8080/v1/orders/1 +``` + +## 💡 开发最佳实践 + +### 1. 错误处理 + +使用项目定义的错误码: + +```go +// internal/ecode/user.go +var ( + ErrUserNotFound = errcode.NewError(20101, "用户不存在") + ErrUserIsExist = errcode.NewError(20102, "用户已存在") + ErrPasswordIncorrect = errcode.NewError(20103, "密码错误") +) + +// 使用示例 +func (s *UserService) GetUser(ctx context.Context, id int64) (*types.User, error) { + user, err := s.repo.GetUser(ctx, id) + if err != nil { + return nil, err + } + if user == nil || user.ID == 0 { + return nil, ecode.ErrUserNotFound // 使用定义的错误码 + } + return user, nil +} +``` + +### 2. 分层原则 + +- **Service 层**: 只包含业务逻辑,不直接操作数据库 +- **Repository 层**: 提供数据访问接口,封装缓存策略 +- **DAL 层**: 直接操作数据库、缓存、RPC + +错误示例(不要这样做): +```go +// ❌ Service 层直接使用 DAO +func (s *UserService) GetUser(ctx context.Context, id int64) { + user, _ := dao.UserInfoModel.WithContext(ctx).First() // 错误! +} +``` + +正确示例: +```go +// ✅ Service 调用 Repository +func (s *UserService) GetUser(ctx context.Context, id int64) { + user, err := s.repo.GetUser(ctx, id) // 正确! +} +``` + +### 3. 缓存策略 + +Repository 层统一处理缓存,避免在 Service 层操作缓存: + +```go +// ✅ 在 Repository 中处理缓存 +func (r *userRepo) GetUser(ctx context.Context, id int64) (*model.UserInfoModel, error) { + // 1. 查本地缓存 + // 2. 查 Redis + // 3. 查数据库并回写缓存 + // Service 层无需关心缓存细节 +} +``` + +### 4. 测试编写 + +为每个层编写单元测试: + +```go +// internal/service/user_svc_test.go +func TestUserService_CreateUser(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockRepo := mocks.NewMockUserRepo(ctrl) + svc := NewUserService(mockRepo) + + mockRepo.EXPECT(). + CreateUser(gomock.Any(), gomock.Any()). + Return(int64(1), nil) + + output, err := svc.CreateUser(context.Background(), types.CreateUserInput{ + Username: "testuser", + Email: "test@example.com", + Password: "password123", + }) + + assert.NoError(t, err) + assert.Equal(t, int64(1), output.ID) +} +``` + +运行测试: +```bash +make test +``` + +## 🔍 故障排查 + +### Wire 相关问题 + +**问题**: `wire: no provider found for XXX` + +**原因**: 缺少依赖的 Provider + +**解决**: +1. 检查对应的 `ProviderSet` 是否包含该依赖 +2. 确认构造函数签名正确 +3. 运行 `make wire` 重新生成 + +### gRPC 相关问题 + +**问题**: `Proto file not found` + +**原因**: Proto 路径配置错误 + +**解决**: 检查 Makefile 中的 `PROTO_PATH` 配置 + +**问题**: `method XXX not implemented` + +**原因**: gRPC 接口方法未实现 + +**解决**: 在 `*_grpc.go` 文件中实现所有 Proto 定义的方法 + +### GORM 相关问题 + +**问题**: `table not found` + +**原因**: 数据库表不存在 + +**解决**: +1. 检查数据库迁移是否执行 +2. 确认 `TableName()` 方法返回正确的表名 + +**问题**: `column not found` + +**原因**: Model 字段和数据库列不匹配 + +**解决**: +1. 检查 gorm tag 是否正确 +2. 运行 `make gorm-gen` 重新生成 DAO + +## 📚 扩展阅读 + +- [Eagle 框架文档](https://go-eagle.org) +- [Google Wire 使用指南](https://github.com/google/wire) +- [gRPC Go 快速开始](https://grpc.io/docs/languages/go/quickstart/) +- [GORM 文档](https://gorm.io) +- [Clean Architecture 原则](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) + +## 🤝 获取帮助 + +- 项目内部文档: `CLAUDE.md` +- 代码注释: 查看相关文件的注释 +- 团队协作: 向有经验的同事请教 + +--- + +**提示**: 本指南生成的 Markdown 文档包含完整的代码示例和详细说明,建议保存为参考文档供新手随时查阅。当你遇到具体问题时,也可以回来查找相应章节。 + +祝你在 Eagle 项目中开发愉快!🚀 diff --git a/.claude/skills/eagle-onboarding/evals/evals.json b/.claude/skills/eagle-onboarding/evals/evals.json new file mode 100644 index 0000000..7dd5c56 --- /dev/null +++ b/.claude/skills/eagle-onboarding/evals/evals.json @@ -0,0 +1,23 @@ +{ + "skill_name": "eagle-onboarding", + "evals": [ + { + "id": 1, + "prompt": "我刚加入这个项目,能给我介绍一下项目的整体架构吗?我看到了 service、repository、dal 这些目录,它们之间是什么关系?", + "expected_output": "包含清晰的三层架构说明(Service -> Repository -> DAL),解释各层职责,并提供目录结构图和简单示例", + "files": [] + }, + { + "id": 2, + "prompt": "我需要添加一个新的 API 接口来获取用户的订单历史记录,应该怎么做?可以给我一个完整的步骤指导吗,包括 proto 定义、service、repository 等", + "expected_output": "完整的开发流程指导,从 proto 定义开始,依次说明每一层的实现步骤,包含具体的代码示例", + "files": [] + }, + { + "id": 3, + "prompt": "Wire 依赖注入是怎么工作的?我修改了代码后需要运行什么命令?如果我想添加一个新的 service,需要在哪里配置依赖关系?", + "expected_output": "解释 Wire 工作原理,说明 wire.go 和 ProviderSet 的作用,提供添加新依赖的具体步骤和示例代码", + "files": [] + } + ] +} From 7edbbf0b439a98a3b04abbf546f43b07866bbeac Mon Sep 17 00:00:00 2001 From: qloog Date: Sun, 26 Apr 2026 16:46:48 +0800 Subject: [PATCH 2/4] feat: add golang skill --- .claude/rules/common/agents.md | 50 ++ .claude/rules/common/code-review.md | 124 ++++ .claude/rules/common/coding-style.md | 48 ++ .claude/rules/common/development-workflow.md | 44 ++ .claude/rules/common/git-workflow.md | 24 + .claude/rules/common/hooks.md | 30 + .claude/rules/common/patterns.md | 31 + .claude/rules/common/performance.md | 55 ++ .claude/rules/common/security.md | 29 + .claude/rules/common/testing.md | 29 + .claude/rules/golang/coding-style.md | 32 + .claude/rules/golang/hooks.md | 17 + .claude/rules/golang/patterns.md | 45 ++ .claude/rules/golang/security.md | 34 + .claude/rules/golang/testing.md | 31 + .claude/skills/golang-patterns/SKILL.md | 674 +++++++++++++++++ .claude/skills/golang-testing/SKILL.md | 720 +++++++++++++++++++ 17 files changed, 2017 insertions(+) create mode 100644 .claude/rules/common/agents.md create mode 100644 .claude/rules/common/code-review.md create mode 100644 .claude/rules/common/coding-style.md create mode 100644 .claude/rules/common/development-workflow.md create mode 100644 .claude/rules/common/git-workflow.md create mode 100644 .claude/rules/common/hooks.md create mode 100644 .claude/rules/common/patterns.md create mode 100644 .claude/rules/common/performance.md create mode 100644 .claude/rules/common/security.md create mode 100644 .claude/rules/common/testing.md create mode 100644 .claude/rules/golang/coding-style.md create mode 100644 .claude/rules/golang/hooks.md create mode 100644 .claude/rules/golang/patterns.md create mode 100644 .claude/rules/golang/security.md create mode 100644 .claude/rules/golang/testing.md create mode 100644 .claude/skills/golang-patterns/SKILL.md create mode 100644 .claude/skills/golang-testing/SKILL.md diff --git a/.claude/rules/common/agents.md b/.claude/rules/common/agents.md new file mode 100644 index 0000000..09d6364 --- /dev/null +++ b/.claude/rules/common/agents.md @@ -0,0 +1,50 @@ +# Agent Orchestration + +## Available Agents + +Located in `~/.claude/agents/`: + +| Agent | Purpose | When to Use | +|-------|---------|-------------| +| planner | Implementation planning | Complex features, refactoring | +| architect | System design | Architectural decisions | +| tdd-guide | Test-driven development | New features, bug fixes | +| code-reviewer | Code review | After writing code | +| security-reviewer | Security analysis | Before commits | +| build-error-resolver | Fix build errors | When build fails | +| e2e-runner | E2E testing | Critical user flows | +| refactor-cleaner | Dead code cleanup | Code maintenance | +| doc-updater | Documentation | Updating docs | +| rust-reviewer | Rust code review | Rust projects | + +## Immediate Agent Usage + +No user prompt needed: +1. Complex feature requests - Use **planner** agent +2. Code just written/modified - Use **code-reviewer** agent +3. Bug fix or new feature - Use **tdd-guide** agent +4. Architectural decision - Use **architect** agent + +## Parallel Task Execution + +ALWAYS use parallel Task execution for independent operations: + +```markdown +# GOOD: Parallel execution +Launch 3 agents in parallel: +1. Agent 1: Security analysis of auth module +2. Agent 2: Performance review of cache system +3. Agent 3: Type checking of utilities + +# BAD: Sequential when unnecessary +First agent 1, then agent 2, then agent 3 +``` + +## Multi-Perspective Analysis + +For complex problems, use split role sub-agents: +- Factual reviewer +- Senior engineer +- Security expert +- Consistency reviewer +- Redundancy checker diff --git a/.claude/rules/common/code-review.md b/.claude/rules/common/code-review.md new file mode 100644 index 0000000..d79ba9b --- /dev/null +++ b/.claude/rules/common/code-review.md @@ -0,0 +1,124 @@ +# Code Review Standards + +## Purpose + +Code review ensures quality, security, and maintainability before code is merged. This rule defines when and how to conduct code reviews. + +## When to Review + +**MANDATORY review triggers:** + +- After writing or modifying code +- Before any commit to shared branches +- When security-sensitive code is changed (auth, payments, user data) +- When architectural changes are made +- Before merging pull requests + +**Pre-Review Requirements:** + +Before requesting review, ensure: + +- All automated checks (CI/CD) are passing +- Merge conflicts are resolved +- Branch is up to date with target branch + +## Review Checklist + +Before marking code complete: + +- [ ] Code is readable and well-named +- [ ] Functions are focused (<50 lines) +- [ ] Files are cohesive (<800 lines) +- [ ] No deep nesting (>4 levels) +- [ ] Errors are handled explicitly +- [ ] No hardcoded secrets or credentials +- [ ] No console.log or debug statements +- [ ] Tests exist for new functionality +- [ ] Test coverage meets 80% minimum + +## Security Review Triggers + +**STOP and use security-reviewer agent when:** + +- Authentication or authorization code +- User input handling +- Database queries +- File system operations +- External API calls +- Cryptographic operations +- Payment or financial code + +## Review Severity Levels + +| Level | Meaning | Action | +|-------|---------|--------| +| CRITICAL | Security vulnerability or data loss risk | **BLOCK** - Must fix before merge | +| HIGH | Bug or significant quality issue | **WARN** - Should fix before merge | +| MEDIUM | Maintainability concern | **INFO** - Consider fixing | +| LOW | Style or minor suggestion | **NOTE** - Optional | + +## Agent Usage + +Use these agents for code review: + +| Agent | Purpose | +|-------|---------| +| **code-reviewer** | General code quality, patterns, best practices | +| **security-reviewer** | Security vulnerabilities, OWASP Top 10 | +| **typescript-reviewer** | TypeScript/JavaScript specific issues | +| **python-reviewer** | Python specific issues | +| **go-reviewer** | Go specific issues | +| **rust-reviewer** | Rust specific issues | + +## Review Workflow + +``` +1. Run git diff to understand changes +2. Check security checklist first +3. Review code quality checklist +4. Run relevant tests +5. Verify coverage >= 80% +6. Use appropriate agent for detailed review +``` + +## Common Issues to Catch + +### Security + +- Hardcoded credentials (API keys, passwords, tokens) +- SQL injection (string concatenation in queries) +- XSS vulnerabilities (unescaped user input) +- Path traversal (unsanitized file paths) +- CSRF protection missing +- Authentication bypasses + +### Code Quality + +- Large functions (>50 lines) - split into smaller +- Large files (>800 lines) - extract modules +- Deep nesting (>4 levels) - use early returns +- Missing error handling - handle explicitly +- Mutation patterns - prefer immutable operations +- Missing tests - add test coverage + +### Performance + +- N+1 queries - use JOINs or batching +- Missing pagination - add LIMIT to queries +- Unbounded queries - add constraints +- Missing caching - cache expensive operations + +## Approval Criteria + +- **Approve**: No CRITICAL or HIGH issues +- **Warning**: Only HIGH issues (merge with caution) +- **Block**: CRITICAL issues found + +## Integration with Other Rules + +This rule works with: + +- [testing.md](testing.md) - Test coverage requirements +- [security.md](security.md) - Security checklist +- [git-workflow.md](git-workflow.md) - Commit standards +- [agents.md](agents.md) - Agent delegation diff --git a/.claude/rules/common/coding-style.md b/.claude/rules/common/coding-style.md new file mode 100644 index 0000000..2ee4fde --- /dev/null +++ b/.claude/rules/common/coding-style.md @@ -0,0 +1,48 @@ +# Coding Style + +## Immutability (CRITICAL) + +ALWAYS create new objects, NEVER mutate existing ones: + +``` +// Pseudocode +WRONG: modify(original, field, value) → changes original in-place +CORRECT: update(original, field, value) → returns new copy with change +``` + +Rationale: Immutable data prevents hidden side effects, makes debugging easier, and enables safe concurrency. + +## File Organization + +MANY SMALL FILES > FEW LARGE FILES: +- High cohesion, low coupling +- 200-400 lines typical, 800 max +- Extract utilities from large modules +- Organize by feature/domain, not by type + +## Error Handling + +ALWAYS handle errors comprehensively: +- Handle errors explicitly at every level +- Provide user-friendly error messages in UI-facing code +- Log detailed error context on the server side +- Never silently swallow errors + +## Input Validation + +ALWAYS validate at system boundaries: +- Validate all user input before processing +- Use schema-based validation where available +- Fail fast with clear error messages +- Never trust external data (API responses, user input, file content) + +## Code Quality Checklist + +Before marking work complete: +- [ ] Code is readable and well-named +- [ ] Functions are small (<50 lines) +- [ ] Files are focused (<800 lines) +- [ ] No deep nesting (>4 levels) +- [ ] Proper error handling +- [ ] No hardcoded values (use constants or config) +- [ ] No mutation (immutable patterns used) diff --git a/.claude/rules/common/development-workflow.md b/.claude/rules/common/development-workflow.md new file mode 100644 index 0000000..ae070be --- /dev/null +++ b/.claude/rules/common/development-workflow.md @@ -0,0 +1,44 @@ +# Development Workflow + +> This file extends [common/git-workflow.md](./git-workflow.md) with the full feature development process that happens before git operations. + +The Feature Implementation Workflow describes the development pipeline: research, planning, TDD, code review, and then committing to git. + +## Feature Implementation Workflow + +0. **Research & Reuse** _(mandatory before any new implementation)_ + - **GitHub code search first:** Run `gh search repos` and `gh search code` to find existing implementations, templates, and patterns before writing anything new. + - **Library docs second:** Use Context7 or primary vendor docs to confirm API behavior, package usage, and version-specific details before implementing. + - **Exa only when the first two are insufficient:** Use Exa for broader web research or discovery after GitHub search and primary docs. + - **Check package registries:** Search npm, PyPI, crates.io, and other registries before writing utility code. Prefer battle-tested libraries over hand-rolled solutions. + - **Search for adaptable implementations:** Look for open-source projects that solve 80%+ of the problem and can be forked, ported, or wrapped. + - Prefer adopting or porting a proven approach over writing net-new code when it meets the requirement. + +1. **Plan First** + - Use **planner** agent to create implementation plan + - Generate planning docs before coding: PRD, architecture, system_design, tech_doc, task_list + - Identify dependencies and risks + - Break down into phases + +2. **TDD Approach** + - Use **tdd-guide** agent + - Write tests first (RED) + - Implement to pass tests (GREEN) + - Refactor (IMPROVE) + - Verify 80%+ coverage + +3. **Code Review** + - Use **code-reviewer** agent immediately after writing code + - Address CRITICAL and HIGH issues + - Fix MEDIUM issues when possible + +4. **Commit & Push** + - Detailed commit messages + - Follow conventional commits format + - See [git-workflow.md](./git-workflow.md) for commit message format and PR process + +5. **Pre-Review Checks** + - Verify all automated checks (CI/CD) are passing + - Resolve any merge conflicts + - Ensure branch is up to date with target branch + - Only request review after these checks pass diff --git a/.claude/rules/common/git-workflow.md b/.claude/rules/common/git-workflow.md new file mode 100644 index 0000000..d57d9e2 --- /dev/null +++ b/.claude/rules/common/git-workflow.md @@ -0,0 +1,24 @@ +# Git Workflow + +## Commit Message Format +``` +: + + +``` + +Types: feat, fix, refactor, docs, test, chore, perf, ci + +Note: Attribution disabled globally via ~/.claude/settings.json. + +## Pull Request Workflow + +When creating PRs: +1. Analyze full commit history (not just latest commit) +2. Use `git diff [base-branch]...HEAD` to see all changes +3. Draft comprehensive PR summary +4. Include test plan with TODOs +5. Push with `-u` flag if new branch + +> For the full development process (planning, TDD, code review) before git operations, +> see [development-workflow.md](./development-workflow.md). diff --git a/.claude/rules/common/hooks.md b/.claude/rules/common/hooks.md new file mode 100644 index 0000000..5439408 --- /dev/null +++ b/.claude/rules/common/hooks.md @@ -0,0 +1,30 @@ +# Hooks System + +## Hook Types + +- **PreToolUse**: Before tool execution (validation, parameter modification) +- **PostToolUse**: After tool execution (auto-format, checks) +- **Stop**: When session ends (final verification) + +## Auto-Accept Permissions + +Use with caution: +- Enable for trusted, well-defined plans +- Disable for exploratory work +- Never use dangerously-skip-permissions flag +- Configure `allowedTools` in `~/.claude.json` instead + +## TodoWrite Best Practices + +Use TodoWrite tool to: +- Track progress on multi-step tasks +- Verify understanding of instructions +- Enable real-time steering +- Show granular implementation steps + +Todo list reveals: +- Out of order steps +- Missing items +- Extra unnecessary items +- Wrong granularity +- Misinterpreted requirements diff --git a/.claude/rules/common/patterns.md b/.claude/rules/common/patterns.md new file mode 100644 index 0000000..959939f --- /dev/null +++ b/.claude/rules/common/patterns.md @@ -0,0 +1,31 @@ +# Common Patterns + +## Skeleton Projects + +When implementing new functionality: +1. Search for battle-tested skeleton projects +2. Use parallel agents to evaluate options: + - Security assessment + - Extensibility analysis + - Relevance scoring + - Implementation planning +3. Clone best match as foundation +4. Iterate within proven structure + +## Design Patterns + +### Repository Pattern + +Encapsulate data access behind a consistent interface: +- Define standard operations: findAll, findById, create, update, delete +- Concrete implementations handle storage details (database, API, file, etc.) +- Business logic depends on the abstract interface, not the storage mechanism +- Enables easy swapping of data sources and simplifies testing with mocks + +### API Response Format + +Use a consistent envelope for all API responses: +- Include a success/status indicator +- Include the data payload (nullable on error) +- Include an error message field (nullable on success) +- Include metadata for paginated responses (total, page, limit) diff --git a/.claude/rules/common/performance.md b/.claude/rules/common/performance.md new file mode 100644 index 0000000..3ffff1b --- /dev/null +++ b/.claude/rules/common/performance.md @@ -0,0 +1,55 @@ +# Performance Optimization + +## Model Selection Strategy + +**Haiku 4.5** (90% of Sonnet capability, 3x cost savings): +- Lightweight agents with frequent invocation +- Pair programming and code generation +- Worker agents in multi-agent systems + +**Sonnet 4.6** (Best coding model): +- Main development work +- Orchestrating multi-agent workflows +- Complex coding tasks + +**Opus 4.5** (Deepest reasoning): +- Complex architectural decisions +- Maximum reasoning requirements +- Research and analysis tasks + +## Context Window Management + +Avoid last 20% of context window for: +- Large-scale refactoring +- Feature implementation spanning multiple files +- Debugging complex interactions + +Lower context sensitivity tasks: +- Single-file edits +- Independent utility creation +- Documentation updates +- Simple bug fixes + +## Extended Thinking + Plan Mode + +Extended thinking is enabled by default, reserving up to 31,999 tokens for internal reasoning. + +Control extended thinking via: +- **Toggle**: Option+T (macOS) / Alt+T (Windows/Linux) +- **Config**: Set `alwaysThinkingEnabled` in `~/.claude/settings.json` +- **Budget cap**: `export MAX_THINKING_TOKENS=10000` +- **Verbose mode**: Ctrl+O to see thinking output + +For complex tasks requiring deep reasoning: +1. Ensure extended thinking is enabled (on by default) +2. Enable **Plan Mode** for structured approach +3. Use multiple critique rounds for thorough analysis +4. Use split role sub-agents for diverse perspectives + +## Build Troubleshooting + +If build fails: +1. Use **build-error-resolver** agent +2. Analyze error messages +3. Fix incrementally +4. Verify after each fix diff --git a/.claude/rules/common/security.md b/.claude/rules/common/security.md new file mode 100644 index 0000000..49624c0 --- /dev/null +++ b/.claude/rules/common/security.md @@ -0,0 +1,29 @@ +# Security Guidelines + +## Mandatory Security Checks + +Before ANY commit: +- [ ] No hardcoded secrets (API keys, passwords, tokens) +- [ ] All user inputs validated +- [ ] SQL injection prevention (parameterized queries) +- [ ] XSS prevention (sanitized HTML) +- [ ] CSRF protection enabled +- [ ] Authentication/authorization verified +- [ ] Rate limiting on all endpoints +- [ ] Error messages don't leak sensitive data + +## Secret Management + +- NEVER hardcode secrets in source code +- ALWAYS use environment variables or a secret manager +- Validate that required secrets are present at startup +- Rotate any secrets that may have been exposed + +## Security Response Protocol + +If security issue found: +1. STOP immediately +2. Use **security-reviewer** agent +3. Fix CRITICAL issues before continuing +4. Rotate any exposed secrets +5. Review entire codebase for similar issues diff --git a/.claude/rules/common/testing.md b/.claude/rules/common/testing.md new file mode 100644 index 0000000..fdcd949 --- /dev/null +++ b/.claude/rules/common/testing.md @@ -0,0 +1,29 @@ +# Testing Requirements + +## Minimum Test Coverage: 80% + +Test Types (ALL required): +1. **Unit Tests** - Individual functions, utilities, components +2. **Integration Tests** - API endpoints, database operations +3. **E2E Tests** - Critical user flows (framework chosen per language) + +## Test-Driven Development + +MANDATORY workflow: +1. Write test first (RED) +2. Run test - it should FAIL +3. Write minimal implementation (GREEN) +4. Run test - it should PASS +5. Refactor (IMPROVE) +6. Verify coverage (80%+) + +## Troubleshooting Test Failures + +1. Use **tdd-guide** agent +2. Check test isolation +3. Verify mocks are correct +4. Fix implementation, not tests (unless tests are wrong) + +## Agent Support + +- **tdd-guide** - Use PROACTIVELY for new features, enforces write-tests-first diff --git a/.claude/rules/golang/coding-style.md b/.claude/rules/golang/coding-style.md new file mode 100644 index 0000000..d7d6c31 --- /dev/null +++ b/.claude/rules/golang/coding-style.md @@ -0,0 +1,32 @@ +--- +paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" +--- +# Go Coding Style + +> This file extends [common/coding-style.md](../common/coding-style.md) with Go specific content. + +## Formatting + +- **gofmt** and **goimports** are mandatory — no style debates + +## Design Principles + +- Accept interfaces, return structs +- Keep interfaces small (1-3 methods) + +## Error Handling + +Always wrap errors with context: + +```go +if err != nil { + return fmt.Errorf("failed to create user: %w", err) +} +``` + +## Reference + +See skill: `golang-patterns` for comprehensive Go idioms and patterns. diff --git a/.claude/rules/golang/hooks.md b/.claude/rules/golang/hooks.md new file mode 100644 index 0000000..f05e4ad --- /dev/null +++ b/.claude/rules/golang/hooks.md @@ -0,0 +1,17 @@ +--- +paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" +--- +# Go Hooks + +> This file extends [common/hooks.md](../common/hooks.md) with Go specific content. + +## PostToolUse Hooks + +Configure in `~/.claude/settings.json`: + +- **gofmt/goimports**: Auto-format `.go` files after edit +- **go vet**: Run static analysis after editing `.go` files +- **staticcheck**: Run extended static checks on modified packages diff --git a/.claude/rules/golang/patterns.md b/.claude/rules/golang/patterns.md new file mode 100644 index 0000000..ba28dba --- /dev/null +++ b/.claude/rules/golang/patterns.md @@ -0,0 +1,45 @@ +--- +paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" +--- +# Go Patterns + +> This file extends [common/patterns.md](../common/patterns.md) with Go specific content. + +## Functional Options + +```go +type Option func(*Server) + +func WithPort(port int) Option { + return func(s *Server) { s.port = port } +} + +func NewServer(opts ...Option) *Server { + s := &Server{port: 8080} + for _, opt := range opts { + opt(s) + } + return s +} +``` + +## Small Interfaces + +Define interfaces where they are used, not where they are implemented. + +## Dependency Injection + +Use constructor functions to inject dependencies: + +```go +func NewUserService(repo UserRepository, logger Logger) *UserService { + return &UserService{repo: repo, logger: logger} +} +``` + +## Reference + +See skill: `golang-patterns` for comprehensive Go patterns including concurrency, error handling, and package organization. diff --git a/.claude/rules/golang/security.md b/.claude/rules/golang/security.md new file mode 100644 index 0000000..372b754 --- /dev/null +++ b/.claude/rules/golang/security.md @@ -0,0 +1,34 @@ +--- +paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" +--- +# Go Security + +> This file extends [common/security.md](../common/security.md) with Go specific content. + +## Secret Management + +```go +apiKey := os.Getenv("OPENAI_API_KEY") +if apiKey == "" { + log.Fatal("OPENAI_API_KEY not configured") +} +``` + +## Security Scanning + +- Use **gosec** for static security analysis: + ```bash + gosec ./... + ``` + +## Context & Timeouts + +Always use `context.Context` for timeout control: + +```go +ctx, cancel := context.WithTimeout(ctx, 5*time.Second) +defer cancel() +``` diff --git a/.claude/rules/golang/testing.md b/.claude/rules/golang/testing.md new file mode 100644 index 0000000..6b80022 --- /dev/null +++ b/.claude/rules/golang/testing.md @@ -0,0 +1,31 @@ +--- +paths: + - "**/*.go" + - "**/go.mod" + - "**/go.sum" +--- +# Go Testing + +> This file extends [common/testing.md](../common/testing.md) with Go specific content. + +## Framework + +Use the standard `go test` with **table-driven tests**. + +## Race Detection + +Always run with the `-race` flag: + +```bash +go test -race ./... +``` + +## Coverage + +```bash +go test -cover ./... +``` + +## Reference + +See skill: `golang-testing` for detailed Go testing patterns and helpers. diff --git a/.claude/skills/golang-patterns/SKILL.md b/.claude/skills/golang-patterns/SKILL.md new file mode 100644 index 0000000..4cfab1a --- /dev/null +++ b/.claude/skills/golang-patterns/SKILL.md @@ -0,0 +1,674 @@ +--- +name: golang-patterns +description: Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. +origin: ECC +--- + +# Go Development Patterns + +Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications. + +## When to Activate + +- Writing new Go code +- Reviewing Go code +- Refactoring existing Go code +- Designing Go packages/modules + +## Core Principles + +### 1. Simplicity and Clarity + +Go favors simplicity over cleverness. Code should be obvious and easy to read. + +```go +// Good: Clear and direct +func GetUser(id string) (*User, error) { + user, err := db.FindUser(id) + if err != nil { + return nil, fmt.Errorf("get user %s: %w", id, err) + } + return user, nil +} + +// Bad: Overly clever +func GetUser(id string) (*User, error) { + return func() (*User, error) { + if u, e := db.FindUser(id); e == nil { + return u, nil + } else { + return nil, e + } + }() +} +``` + +### 2. Make the Zero Value Useful + +Design types so their zero value is immediately usable without initialization. + +```go +// Good: Zero value is useful +type Counter struct { + mu sync.Mutex + count int // zero value is 0, ready to use +} + +func (c *Counter) Inc() { + c.mu.Lock() + c.count++ + c.mu.Unlock() +} + +// Good: bytes.Buffer works with zero value +var buf bytes.Buffer +buf.WriteString("hello") + +// Bad: Requires initialization +type BadCounter struct { + counts map[string]int // nil map will panic +} +``` + +### 3. Accept Interfaces, Return Structs + +Functions should accept interface parameters and return concrete types. + +```go +// Good: Accepts interface, returns concrete type +func ProcessData(r io.Reader) (*Result, error) { + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return &Result{Data: data}, nil +} + +// Bad: Returns interface (hides implementation details unnecessarily) +func ProcessData(r io.Reader) (io.Reader, error) { + // ... +} +``` + +## Error Handling Patterns + +### Error Wrapping with Context + +```go +// Good: Wrap errors with context +func LoadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("load config %s: %w", path, err) + } + + var cfg Config + if err := json.Unmarshal(data, &cfg); err != nil { + return nil, fmt.Errorf("parse config %s: %w", path, err) + } + + return &cfg, nil +} +``` + +### Custom Error Types + +```go +// Define domain-specific errors +type ValidationError struct { + Field string + Message string +} + +func (e *ValidationError) Error() string { + return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message) +} + +// Sentinel errors for common cases +var ( + ErrNotFound = errors.New("resource not found") + ErrUnauthorized = errors.New("unauthorized") + ErrInvalidInput = errors.New("invalid input") +) +``` + +### Error Checking with errors.Is and errors.As + +```go +func HandleError(err error) { + // Check for specific error + if errors.Is(err, sql.ErrNoRows) { + log.Println("No records found") + return + } + + // Check for error type + var validationErr *ValidationError + if errors.As(err, &validationErr) { + log.Printf("Validation error on field %s: %s", + validationErr.Field, validationErr.Message) + return + } + + // Unknown error + log.Printf("Unexpected error: %v", err) +} +``` + +### Never Ignore Errors + +```go +// Bad: Ignoring error with blank identifier +result, _ := doSomething() + +// Good: Handle or explicitly document why it's safe to ignore +result, err := doSomething() +if err != nil { + return err +} + +// Acceptable: When error truly doesn't matter (rare) +_ = writer.Close() // Best-effort cleanup, error logged elsewhere +``` + +## Concurrency Patterns + +### Worker Pool + +```go +func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) { + var wg sync.WaitGroup + + for i := 0; i < numWorkers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for job := range jobs { + results <- process(job) + } + }() + } + + wg.Wait() + close(results) +} +``` + +### Context for Cancellation and Timeouts + +```go +func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return nil, fmt.Errorf("create request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("fetch %s: %w", url, err) + } + defer resp.Body.Close() + + return io.ReadAll(resp.Body) +} +``` + +### Graceful Shutdown + +```go +func GracefulShutdown(server *http.Server) { + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + + <-quit + log.Println("Shutting down server...") + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := server.Shutdown(ctx); err != nil { + log.Fatalf("Server forced to shutdown: %v", err) + } + + log.Println("Server exited") +} +``` + +### errgroup for Coordinated Goroutines + +```go +import "golang.org/x/sync/errgroup" + +func FetchAll(ctx context.Context, urls []string) ([][]byte, error) { + g, ctx := errgroup.WithContext(ctx) + results := make([][]byte, len(urls)) + + for i, url := range urls { + i, url := i, url // Capture loop variables + g.Go(func() error { + data, err := FetchWithTimeout(ctx, url) + if err != nil { + return err + } + results[i] = data + return nil + }) + } + + if err := g.Wait(); err != nil { + return nil, err + } + return results, nil +} +``` + +### Avoiding Goroutine Leaks + +```go +// Bad: Goroutine leak if context is cancelled +func leakyFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte) + go func() { + data, _ := fetch(url) + ch <- data // Blocks forever if no receiver + }() + return ch +} + +// Good: Properly handles cancellation +func safeFetch(ctx context.Context, url string) <-chan []byte { + ch := make(chan []byte, 1) // Buffered channel + go func() { + data, err := fetch(url) + if err != nil { + return + } + select { + case ch <- data: + case <-ctx.Done(): + } + }() + return ch +} +``` + +## Interface Design + +### Small, Focused Interfaces + +```go +// Good: Single-method interfaces +type Reader interface { + Read(p []byte) (n int, err error) +} + +type Writer interface { + Write(p []byte) (n int, err error) +} + +type Closer interface { + Close() error +} + +// Compose interfaces as needed +type ReadWriteCloser interface { + Reader + Writer + Closer +} +``` + +### Define Interfaces Where They're Used + +```go +// In the consumer package, not the provider +package service + +// UserStore defines what this service needs +type UserStore interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +type Service struct { + store UserStore +} + +// Concrete implementation can be in another package +// It doesn't need to know about this interface +``` + +### Optional Behavior with Type Assertions + +```go +type Flusher interface { + Flush() error +} + +func WriteAndFlush(w io.Writer, data []byte) error { + if _, err := w.Write(data); err != nil { + return err + } + + // Flush if supported + if f, ok := w.(Flusher); ok { + return f.Flush() + } + return nil +} +``` + +## Package Organization + +### Standard Project Layout + +```text +myproject/ +├── cmd/ +│ └── myapp/ +│ └── main.go # Entry point +├── internal/ +│ ├── handler/ # HTTP handlers +│ ├── service/ # Business logic +│ ├── repository/ # Data access +│ └── config/ # Configuration +├── pkg/ +│ └── client/ # Public API client +├── api/ +│ └── v1/ # API definitions (proto, OpenAPI) +├── testdata/ # Test fixtures +├── go.mod +├── go.sum +└── Makefile +``` + +### Package Naming + +```go +// Good: Short, lowercase, no underscores +package http +package json +package user + +// Bad: Verbose, mixed case, or redundant +package httpHandler +package json_parser +package userService // Redundant 'Service' suffix +``` + +### Avoid Package-Level State + +```go +// Bad: Global mutable state +var db *sql.DB + +func init() { + db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL")) +} + +// Good: Dependency injection +type Server struct { + db *sql.DB +} + +func NewServer(db *sql.DB) *Server { + return &Server{db: db} +} +``` + +## Struct Design + +### Functional Options Pattern + +```go +type Server struct { + addr string + timeout time.Duration + logger *log.Logger +} + +type Option func(*Server) + +func WithTimeout(d time.Duration) Option { + return func(s *Server) { + s.timeout = d + } +} + +func WithLogger(l *log.Logger) Option { + return func(s *Server) { + s.logger = l + } +} + +func NewServer(addr string, opts ...Option) *Server { + s := &Server{ + addr: addr, + timeout: 30 * time.Second, // default + logger: log.Default(), // default + } + for _, opt := range opts { + opt(s) + } + return s +} + +// Usage +server := NewServer(":8080", + WithTimeout(60*time.Second), + WithLogger(customLogger), +) +``` + +### Embedding for Composition + +```go +type Logger struct { + prefix string +} + +func (l *Logger) Log(msg string) { + fmt.Printf("[%s] %s\n", l.prefix, msg) +} + +type Server struct { + *Logger // Embedding - Server gets Log method + addr string +} + +func NewServer(addr string) *Server { + return &Server{ + Logger: &Logger{prefix: "SERVER"}, + addr: addr, + } +} + +// Usage +s := NewServer(":8080") +s.Log("Starting...") // Calls embedded Logger.Log +``` + +## Memory and Performance + +### Preallocate Slices When Size is Known + +```go +// Bad: Grows slice multiple times +func processItems(items []Item) []Result { + var results []Result + for _, item := range items { + results = append(results, process(item)) + } + return results +} + +// Good: Single allocation +func processItems(items []Item) []Result { + results := make([]Result, 0, len(items)) + for _, item := range items { + results = append(results, process(item)) + } + return results +} +``` + +### Use sync.Pool for Frequent Allocations + +```go +var bufferPool = sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, +} + +func ProcessRequest(data []byte) []byte { + buf := bufferPool.Get().(*bytes.Buffer) + defer func() { + buf.Reset() + bufferPool.Put(buf) + }() + + buf.Write(data) + // Process... + return buf.Bytes() +} +``` + +### Avoid String Concatenation in Loops + +```go +// Bad: Creates many string allocations +func join(parts []string) string { + var result string + for _, p := range parts { + result += p + "," + } + return result +} + +// Good: Single allocation with strings.Builder +func join(parts []string) string { + var sb strings.Builder + for i, p := range parts { + if i > 0 { + sb.WriteString(",") + } + sb.WriteString(p) + } + return sb.String() +} + +// Best: Use standard library +func join(parts []string) string { + return strings.Join(parts, ",") +} +``` + +## Go Tooling Integration + +### Essential Commands + +```bash +# Build and run +go build ./... +go run ./cmd/myapp + +# Testing +go test ./... +go test -race ./... +go test -cover ./... + +# Static analysis +go vet ./... +staticcheck ./... +golangci-lint run + +# Module management +go mod tidy +go mod verify + +# Formatting +gofmt -w . +goimports -w . +``` + +### Recommended Linter Configuration (.golangci.yml) + +```yaml +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - gofmt + - goimports + - misspell + - unconvert + - unparam + +linters-settings: + errcheck: + check-type-assertions: true + govet: + check-shadowing: true + +issues: + exclude-use-default: false +``` + +## Quick Reference: Go Idioms + +| Idiom | Description | +|-------|-------------| +| Accept interfaces, return structs | Functions accept interface params, return concrete types | +| Errors are values | Treat errors as first-class values, not exceptions | +| Don't communicate by sharing memory | Use channels for coordination between goroutines | +| Make the zero value useful | Types should work without explicit initialization | +| A little copying is better than a little dependency | Avoid unnecessary external dependencies | +| Clear is better than clever | Prioritize readability over cleverness | +| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports | +| Return early | Handle errors first, keep happy path unindented | + +## Anti-Patterns to Avoid + +```go +// Bad: Naked returns in long functions +func process() (result int, err error) { + // ... 50 lines ... + return // What is being returned? +} + +// Bad: Using panic for control flow +func GetUser(id string) *User { + user, err := db.Find(id) + if err != nil { + panic(err) // Don't do this + } + return user +} + +// Bad: Passing context in struct +type Request struct { + ctx context.Context // Context should be first param + ID string +} + +// Good: Context as first parameter +func ProcessRequest(ctx context.Context, id string) error { + // ... +} + +// Bad: Mixing value and pointer receivers +type Counter struct{ n int } +func (c Counter) Value() int { return c.n } // Value receiver +func (c *Counter) Increment() { c.n++ } // Pointer receiver +// Pick one style and be consistent +``` + +**Remember**: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple. \ No newline at end of file diff --git a/.claude/skills/golang-testing/SKILL.md b/.claude/skills/golang-testing/SKILL.md new file mode 100644 index 0000000..3e6638d --- /dev/null +++ b/.claude/skills/golang-testing/SKILL.md @@ -0,0 +1,720 @@ +--- +name: golang-testing +description: Go testing patterns including table-driven tests, subtests, benchmarks, fuzzing, and test coverage. Follows TDD methodology with idiomatic Go practices. +origin: ECC +--- + +# Go Testing Patterns + +Comprehensive Go testing patterns for writing reliable, maintainable tests following TDD methodology. + +## When to Activate + +- Writing new Go functions or methods +- Adding test coverage to existing code +- Creating benchmarks for performance-critical code +- Implementing fuzz tests for input validation +- Following TDD workflow in Go projects + +## TDD Workflow for Go + +### The RED-GREEN-REFACTOR Cycle + +``` +RED → Write a failing test first +GREEN → Write minimal code to pass the test +REFACTOR → Improve code while keeping tests green +REPEAT → Continue with next requirement +``` + +### Step-by-Step TDD in Go + +```go +// Step 1: Define the interface/signature +// calculator.go +package calculator + +func Add(a, b int) int { + panic("not implemented") // Placeholder +} + +// Step 2: Write failing test (RED) +// calculator_test.go +package calculator + +import "testing" + +func TestAdd(t *testing.T) { + got := Add(2, 3) + want := 5 + if got != want { + t.Errorf("Add(2, 3) = %d; want %d", got, want) + } +} + +// Step 3: Run test - verify FAIL +// $ go test +// --- FAIL: TestAdd (0.00s) +// panic: not implemented + +// Step 4: Implement minimal code (GREEN) +func Add(a, b int) int { + return a + b +} + +// Step 5: Run test - verify PASS +// $ go test +// PASS + +// Step 6: Refactor if needed, verify tests still pass +``` + +## Table-Driven Tests + +The standard pattern for Go tests. Enables comprehensive coverage with minimal code. + +```go +func TestAdd(t *testing.T) { + tests := []struct { + name string + a, b int + expected int + }{ + {"positive numbers", 2, 3, 5}, + {"negative numbers", -1, -2, -3}, + {"zero values", 0, 0, 0}, + {"mixed signs", -1, 1, 0}, + {"large numbers", 1000000, 2000000, 3000000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.a, tt.b) + if got != tt.expected { + t.Errorf("Add(%d, %d) = %d; want %d", + tt.a, tt.b, got, tt.expected) + } + }) + } +} +``` + +### Table-Driven Tests with Error Cases + +```go +func TestParseConfig(t *testing.T) { + tests := []struct { + name string + input string + want *Config + wantErr bool + }{ + { + name: "valid config", + input: `{"host": "localhost", "port": 8080}`, + want: &Config{Host: "localhost", Port: 8080}, + }, + { + name: "invalid JSON", + input: `{invalid}`, + wantErr: true, + }, + { + name: "empty input", + input: "", + wantErr: true, + }, + { + name: "minimal config", + input: `{}`, + want: &Config{}, // Zero value config + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseConfig(tt.input) + + if tt.wantErr { + if err == nil { + t.Error("expected error, got nil") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("got %+v; want %+v", got, tt.want) + } + }) + } +} +``` + +## Subtests and Sub-benchmarks + +### Organizing Related Tests + +```go +func TestUser(t *testing.T) { + // Setup shared by all subtests + db := setupTestDB(t) + + t.Run("Create", func(t *testing.T) { + user := &User{Name: "Alice"} + err := db.CreateUser(user) + if err != nil { + t.Fatalf("CreateUser failed: %v", err) + } + if user.ID == "" { + t.Error("expected user ID to be set") + } + }) + + t.Run("Get", func(t *testing.T) { + user, err := db.GetUser("alice-id") + if err != nil { + t.Fatalf("GetUser failed: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } + }) + + t.Run("Update", func(t *testing.T) { + // ... + }) + + t.Run("Delete", func(t *testing.T) { + // ... + }) +} +``` + +### Parallel Subtests + +```go +func TestParallel(t *testing.T) { + tests := []struct { + name string + input string + }{ + {"case1", "input1"}, + {"case2", "input2"}, + {"case3", "input3"}, + } + + for _, tt := range tests { + tt := tt // Capture range variable + t.Run(tt.name, func(t *testing.T) { + t.Parallel() // Run subtests in parallel + result := Process(tt.input) + // assertions... + _ = result + }) + } +} +``` + +## Test Helpers + +### Helper Functions + +```go +func setupTestDB(t *testing.T) *sql.DB { + t.Helper() // Marks this as a helper function + + db, err := sql.Open("sqlite3", ":memory:") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + + // Cleanup when test finishes + t.Cleanup(func() { + db.Close() + }) + + // Run migrations + if _, err := db.Exec(schema); err != nil { + t.Fatalf("failed to create schema: %v", err) + } + + return db +} + +func assertNoError(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func assertEqual[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf("got %v; want %v", got, want) + } +} +``` + +### Temporary Files and Directories + +```go +func TestFileProcessing(t *testing.T) { + // Create temp directory - automatically cleaned up + tmpDir := t.TempDir() + + // Create test file + testFile := filepath.Join(tmpDir, "test.txt") + err := os.WriteFile(testFile, []byte("test content"), 0644) + if err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + // Run test + result, err := ProcessFile(testFile) + if err != nil { + t.Fatalf("ProcessFile failed: %v", err) + } + + // Assert... + _ = result +} +``` + +## Golden Files + +Testing against expected output files stored in `testdata/`. + +```go +var update = flag.Bool("update", false, "update golden files") + +func TestRender(t *testing.T) { + tests := []struct { + name string + input Template + }{ + {"simple", Template{Name: "test"}}, + {"complex", Template{Name: "test", Items: []string{"a", "b"}}}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Render(tt.input) + + golden := filepath.Join("testdata", tt.name+".golden") + + if *update { + // Update golden file: go test -update + err := os.WriteFile(golden, got, 0644) + if err != nil { + t.Fatalf("failed to update golden file: %v", err) + } + } + + want, err := os.ReadFile(golden) + if err != nil { + t.Fatalf("failed to read golden file: %v", err) + } + + if !bytes.Equal(got, want) { + t.Errorf("output mismatch:\ngot:\n%s\nwant:\n%s", got, want) + } + }) + } +} +``` + +## Mocking with Interfaces + +### Interface-Based Mocking + +```go +// Define interface for dependencies +type UserRepository interface { + GetUser(id string) (*User, error) + SaveUser(user *User) error +} + +// Production implementation +type PostgresUserRepository struct { + db *sql.DB +} + +func (r *PostgresUserRepository) GetUser(id string) (*User, error) { + // Real database query +} + +// Mock implementation for tests +type MockUserRepository struct { + GetUserFunc func(id string) (*User, error) + SaveUserFunc func(user *User) error +} + +func (m *MockUserRepository) GetUser(id string) (*User, error) { + return m.GetUserFunc(id) +} + +func (m *MockUserRepository) SaveUser(user *User) error { + return m.SaveUserFunc(user) +} + +// Test using mock +func TestUserService(t *testing.T) { + mock := &MockUserRepository{ + GetUserFunc: func(id string) (*User, error) { + if id == "123" { + return &User{ID: "123", Name: "Alice"}, nil + } + return nil, ErrNotFound + }, + } + + service := NewUserService(mock) + + user, err := service.GetUserProfile("123") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.Name != "Alice" { + t.Errorf("got name %q; want %q", user.Name, "Alice") + } +} +``` + +## Benchmarks + +### Basic Benchmarks + +```go +func BenchmarkProcess(b *testing.B) { + data := generateTestData(1000) + b.ResetTimer() // Don't count setup time + + for i := 0; i < b.N; i++ { + Process(data) + } +} + +// Run: go test -bench=BenchmarkProcess -benchmem +// Output: BenchmarkProcess-8 10000 105234 ns/op 4096 B/op 10 allocs/op +``` + +### Benchmark with Different Sizes + +```go +func BenchmarkSort(b *testing.B) { + sizes := []int{100, 1000, 10000, 100000} + + for _, size := range sizes { + b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) { + data := generateRandomSlice(size) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + // Make a copy to avoid sorting already sorted data + tmp := make([]int, len(data)) + copy(tmp, data) + sort.Ints(tmp) + } + }) + } +} +``` + +### Memory Allocation Benchmarks + +```go +func BenchmarkStringConcat(b *testing.B) { + parts := []string{"hello", "world", "foo", "bar", "baz"} + + b.Run("plus", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var s string + for _, p := range parts { + s += p + } + _ = s + } + }) + + b.Run("builder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + var sb strings.Builder + for _, p := range parts { + sb.WriteString(p) + } + _ = sb.String() + } + }) + + b.Run("join", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = strings.Join(parts, "") + } + }) +} +``` + +## Fuzzing (Go 1.18+) + +### Basic Fuzz Test + +```go +func FuzzParseJSON(f *testing.F) { + // Add seed corpus + f.Add(`{"name": "test"}`) + f.Add(`{"count": 123}`) + f.Add(`[]`) + f.Add(`""`) + + f.Fuzz(func(t *testing.T, input string) { + var result map[string]interface{} + err := json.Unmarshal([]byte(input), &result) + + if err != nil { + // Invalid JSON is expected for random input + return + } + + // If parsing succeeded, re-encoding should work + _, err = json.Marshal(result) + if err != nil { + t.Errorf("Marshal failed after successful Unmarshal: %v", err) + } + }) +} + +// Run: go test -fuzz=FuzzParseJSON -fuzztime=30s +``` + +### Fuzz Test with Multiple Inputs + +```go +func FuzzCompare(f *testing.F) { + f.Add("hello", "world") + f.Add("", "") + f.Add("abc", "abc") + + f.Fuzz(func(t *testing.T, a, b string) { + result := Compare(a, b) + + // Property: Compare(a, a) should always equal 0 + if a == b && result != 0 { + t.Errorf("Compare(%q, %q) = %d; want 0", a, b, result) + } + + // Property: Compare(a, b) and Compare(b, a) should have opposite signs + reverse := Compare(b, a) + if (result > 0 && reverse >= 0) || (result < 0 && reverse <= 0) { + if result != 0 || reverse != 0 { + t.Errorf("Compare(%q, %q) = %d, Compare(%q, %q) = %d; inconsistent", + a, b, result, b, a, reverse) + } + } + }) +} +``` + +## Test Coverage + +### Running Coverage + +```bash +# Basic coverage +go test -cover ./... + +# Generate coverage profile +go test -coverprofile=coverage.out ./... + +# View coverage in browser +go tool cover -html=coverage.out + +# View coverage by function +go tool cover -func=coverage.out + +# Coverage with race detection +go test -race -coverprofile=coverage.out ./... +``` + +### Coverage Targets + +| Code Type | Target | +|-----------|--------| +| Critical business logic | 100% | +| Public APIs | 90%+ | +| General code | 80%+ | +| Generated code | Exclude | + +### Excluding Generated Code from Coverage + +```go +//go:generate mockgen -source=interface.go -destination=mock_interface.go + +// In coverage profile, exclude with build tags: +// go test -cover -tags=!generate ./... +``` + +## HTTP Handler Testing + +```go +func TestHealthHandler(t *testing.T) { + // Create request + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + // Call handler + HealthHandler(w, req) + + // Check response + resp := w.Result() + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("got status %d; want %d", resp.StatusCode, http.StatusOK) + } + + body, _ := io.ReadAll(resp.Body) + if string(body) != "OK" { + t.Errorf("got body %q; want %q", body, "OK") + } +} + +func TestAPIHandler(t *testing.T) { + tests := []struct { + name string + method string + path string + body string + wantStatus int + wantBody string + }{ + { + name: "get user", + method: http.MethodGet, + path: "/users/123", + wantStatus: http.StatusOK, + wantBody: `{"id":"123","name":"Alice"}`, + }, + { + name: "not found", + method: http.MethodGet, + path: "/users/999", + wantStatus: http.StatusNotFound, + }, + { + name: "create user", + method: http.MethodPost, + path: "/users", + body: `{"name":"Bob"}`, + wantStatus: http.StatusCreated, + }, + } + + handler := NewAPIHandler() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var body io.Reader + if tt.body != "" { + body = strings.NewReader(tt.body) + } + + req := httptest.NewRequest(tt.method, tt.path, body) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + + if w.Code != tt.wantStatus { + t.Errorf("got status %d; want %d", w.Code, tt.wantStatus) + } + + if tt.wantBody != "" && w.Body.String() != tt.wantBody { + t.Errorf("got body %q; want %q", w.Body.String(), tt.wantBody) + } + }) + } +} +``` + +## Testing Commands + +```bash +# Run all tests +go test ./... + +# Run tests with verbose output +go test -v ./... + +# Run specific test +go test -run TestAdd ./... + +# Run tests matching pattern +go test -run "TestUser/Create" ./... + +# Run tests with race detector +go test -race ./... + +# Run tests with coverage +go test -cover -coverprofile=coverage.out ./... + +# Run short tests only +go test -short ./... + +# Run tests with timeout +go test -timeout 30s ./... + +# Run benchmarks +go test -bench=. -benchmem ./... + +# Run fuzzing +go test -fuzz=FuzzParse -fuzztime=30s ./... + +# Count test runs (for flaky test detection) +go test -count=10 ./... +``` + +## Best Practices + +**DO:** +- Write tests FIRST (TDD) +- Use table-driven tests for comprehensive coverage +- Test behavior, not implementation +- Use `t.Helper()` in helper functions +- Use `t.Parallel()` for independent tests +- Clean up resources with `t.Cleanup()` +- Use meaningful test names that describe the scenario + +**DON'T:** +- Test private functions directly (test through public API) +- Use `time.Sleep()` in tests (use channels or conditions) +- Ignore flaky tests (fix or remove them) +- Mock everything (prefer integration tests when possible) +- Skip error path testing + +## Integration with CI/CD + +```yaml +# GitHub Actions example +test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Run tests + run: go test -race -coverprofile=coverage.out ./... + + - name: Check coverage + run: | + go tool cover -func=coverage.out | grep total | awk '{print $3}' | \ + awk -F'%' '{if ($1 < 80) exit 1}' +``` + +**Remember**: Tests are documentation. They show how your code is meant to be used. Write them clearly and keep them up to date. \ No newline at end of file From a122738f74be3afbcac7c6ccdfcaa52bc27bba75 Mon Sep 17 00:00:00 2001 From: qloog Date: Sat, 18 Jul 2026 15:06:19 +0800 Subject: [PATCH 3/4] chore --- .gitignore | 1 + CLAUDE.md | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 354958b..0fcc56c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ coverage.out internal/dal/db/dao/gen_test.db bin .claude/settings.local.json +eagle-course diff --git a/CLAUDE.md b/CLAUDE.md index 6f5e577..4348576 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,6 +2,93 @@ 此文件为 Claude Code (claude.ai/code) 在此代码库中工作时提供指导。 +## 技术栈 (Tech Stack) + +- **语言 (Language)**: Go 1.22 +- **应用框架 (Framework)**: [Eagle](https://github.com/go-eagle/eagle) 微服务框架 (`go-eagle/eagle`) +- **Web/HTTP**: Gin (`gin-gonic/gin`) + Swagger 文档 (`swaggo/gin-swagger`) +- **RPC / 协议 (RPC / Protocol)**: gRPC (`google.golang.org/grpc`) + Protocol Buffers,带 `protoc-gen-validate` 参数校验 +- **依赖注入 (DI)**: Google Wire (`google/wire`),编译时注入 +- **数据库 ORM (Database)**: GORM (`gorm.io/gorm`) + `gorm/gen` 代码生成 + `dbresolver` 读写分离;驱动含 SQLite,另支持 ClickHouse +- **缓存 (Cache)**: Redis (`redis/go-redis/v9`) +- **消息队列 / 任务 (MQ / Task Queue)**: Asynq (`hibiken/asynq`,基于 Redis) + RabbitMQ (`rabbitmq/amqp091-go`) +- **可观测性 (Observability)**: OpenTelemetry (`go.opentelemetry.io/otel`) 链路追踪 + Prometheus (`prometheus/client_golang`) 指标 +- **工具库 (Utilities)**: `jinzhu/copier`(对象拷贝)、`pkg/errors`、`golang.org/x/sync` +- **架构 (Architecture)**: Clean Architecture(Service → Repository → DAL) + +## 目录结构 (Directory Structure) + +``` +. +├── api/ # Proto 接口定义与生成代码(gRPC/HTTP/校验) +│ ├── helloworld/ # 示例服务的 proto 定义 +│ └── user/ # 用户服务的 proto 定义 +├── cmd/ # 程序入口 +│ ├── server/ # HTTP/gRPC 主服务入口(含 wire 依赖注入) +│ ├── consumer/ # 后台任务消费者入口 +│ └── gen/ # GORM 模型代码生成工具 +├── config/ # 配置文件,按环境分目录 +│ ├── dev/ # 开发环境配置 +│ ├── test/ # 测试环境配置 +│ ├── prod/ # 生产环境配置 +│ └── docker/ # Docker 环境配置 +├── internal/ # 私有业务代码(不对外暴露) +│ ├── service/ # 业务逻辑层(*_svc.go 业务、*_grpc.go 协议转换) +│ ├── repository/ # 仓储层,统一数据访问接口 +│ ├── dal/ # 数据访问层(db 数据库 / cache 缓存 / rpc 外部调用) +│ ├── handler/ # HTTP 请求处理器 +│ ├── routers/ # 路由注册 +│ ├── server/ # 服务器启动装配(HTTP/gRPC) +│ ├── tasks/ # 异步任务定义(Asynq) +│ ├── event/ # 事件处理(消息队列) +│ ├── types/ # 请求/响应等类型定义 +│ ├── ecode/ # 业务错误码定义 +│ └── mocks/ # 测试用 mock 代码 +├── deploy/ # 部署相关 +│ ├── docker/ # Dockerfile 等镜像构建文件 +│ ├── docker-compose/ # docker-compose 编排文件 +│ └── k8s/ # Kubernetes 部署清单 +├── third_party/ # 第三方 proto 依赖(google/gogo/validate 等) +├── scripts/ # 构建与运维脚本 +├── docs/ # Swagger 生成的接口文档 +└── web/ # 前端/静态资源 +``` + +## 编码规范 (Coding Standards) + +### 语言与注释 (Language & Comments) +- 与用户交流一律用**中文**,代码注释一律用**英文** +- 命名清晰达意,遵循 Go 官方命名惯例(导出用大驼峰、非导出用小驼峰) + +### 设计原则 (Design Principles) +- **不要过度设计**:保证代码简洁易懂、简单实用 +- **最小化改动**:改动时尽量不影响其他模块,控制圈复杂度 +- **模块化与复用**:注意模块边界,代码尽可能复用,合理使用设计模式 +- **分层依赖**:严格遵循 Service → Repository → DAL 的依赖方向,业务逻辑依赖抽象接口而非具体存储 +- **不可变性 (Immutability)**:优先返回新对象,避免原地修改,减少隐藏副作用 + +### 文件组织 (File Organization) +- 多个小文件优于少数大文件,按功能/领域组织而非按类型 +- 单文件一般 200-400 行,最多不超过 800 行 +- 函数保持短小(建议 <50 行),嵌套层级不超过 4 层,善用 early return + +### 错误处理 (Error Handling) +- 每一层都显式处理错误,禁止静默吞掉错误 +- 服务端记录详细错误上下文,对外返回友好且不泄露敏感信息的错误 +- 使用 `internal/ecode` 统一管理业务错误码 + +### 输入校验 (Input Validation) +- 在系统边界(API 入口)校验所有外部输入,借助 proto `validate` 规则 +- 快速失败并给出清晰的错误信息,不信任任何外部数据 + +### 安全 (Security) +- 严禁硬编码密钥/密码/Token,统一用环境变量或配置管理 +- 数据库使用参数化查询,防止 SQL 注入 + +### 提交规范 (Commit Convention) +- 遵循 Conventional Commits:`: ` +- type 可选:`feat` / `fix` / `refactor` / `docs` / `test` / `chore` / `perf` / `ci` + ## 常用开发命令 (Common Development Commands) ### 构建与运行 (Building and Running) @@ -80,10 +167,47 @@ 5. 测试 → `make test` 6. 构建 → `make build` -## 注意事项(system prompt) +## Never 规则 (Never Rules) + +以下为**绝对禁止**事项,任何情况下都不得违反: + +### 交流与注释 (Communication & Comments) +- **Never** 用中文以外的语言回复用户(代码注释除外,注释一律用英文) +- **Never** 用中文写代码注释 + +### 代码设计 (Code Design) +- **Never** 过度设计;保证代码简洁易懂、简单实用 +- **Never** 忽视圈复杂度;重复代码应尽量复用 +- **Never** 在改动时波及无关模块,坚持最小化修改 +- **Never** 原地修改传入对象;优先返回新对象(保持不可变) +- **Never** 让单文件超过 800 行、函数超过 50 行、嵌套超过 4 层 + +### 分层与架构 (Layering & Architecture) +- **Never** 跨层反向依赖;严格遵循 Service → Repository → DAL 的依赖方向 +- **Never** 在 Service 层直接操作数据库/缓存,必须经由 Repository 抽象 +- **Never** 手改由 `make wire` / `make grpc` / `make proto` / `make gorm-gen` 生成的文件(如 `wire_gen.go`、`*.pb.go`) + +### 错误与校验 (Errors & Validation) +- **Never** 静默吞掉错误;每一层都必须显式处理 +- **Never** 信任外部输入;必须在系统边界完成校验 +- **Never** 在对外错误信息中泄露敏感数据 + +### 安全 (Security) +- **Never** 硬编码密钥/密码/Token,统一用环境变量或配置管理 +- **Never** 使用字符串拼接 SQL;必须参数化查询 +- **Never** 提交调试语句、临时打印或注释掉的死代码 + +### 提交 (Commit) +- **Never** 使用不符合 Conventional Commits 规范的提交信息 +- **Never** 在未通过 `make lint` 和 `make test` 前提交代码 + +## Commit 规范 + +- 格式:type(scope): description +- 类型:feat / fix / docs / style / refactor / test / chore + +## 多 Agent 并发 -- Always respond in 中文,但注释一律用英文 -- 不要过度设计,保证代码简洁易懂,简单实用 -- 写代码时,要注意圈复杂度,代码尽可能复用 -- 写代码时,注意模块设计,尽量使用设计模式 -- 改动时最小化修改,尽量不修改到其他模块代码 \ No newline at end of file +- 禁止 git stash +- 禁止切换分支 +- 只 commit 自己修改的文件 From b937a755d62c9c7ef39b9a13a26dc83ebec5de32 Mon Sep 17 00:00:00 2001 From: qloog Date: Sat, 18 Jul 2026 15:07:04 +0800 Subject: [PATCH 4/4] chore: improve content --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4348576..6c43712 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ ## 技术栈 (Tech Stack) -- **语言 (Language)**: Go 1.22 +- **语言 (Language)**: Go 1.22+ - **应用框架 (Framework)**: [Eagle](https://github.com/go-eagle/eagle) 微服务框架 (`go-eagle/eagle`) - **Web/HTTP**: Gin (`gin-gonic/gin`) + Swagger 文档 (`swaggo/gin-swagger`) - **RPC / 协议 (RPC / Protocol)**: gRPC (`google.golang.org/grpc`) + Protocol Buffers,带 `protoc-gen-validate` 参数校验