Go 微服务 — 项目 CLAUDE.md¶
使用 PostgreSQL、gRPC、Docker 的 Go 微服务实战示例。 复制到项目根目录,并根据服务需求进行定制。
项目概述¶
技术栈: Go 1.22+、PostgreSQL、gRPC + REST(grpc-gateway)、Docker、sqlc(类型安全 SQL)、Wire(依赖注入)
架构: 由 domain、repository、service、handler 层组成的整洁架构。使用 gRPC 作为默认传输协议,并为外部客户端提供 REST gateway。
必备规则¶
Go 规则¶
- 遵循 Effective Go 和 Go Code Review Comments 指南
- 使用
errors.New/fmt.Errorf和%w进行错误包装 — 不要对错误进行字符串匹配 - 禁止使用
init()函数 — 在main()或构造函数中显式初始化 - 禁止全局可变状态 — 通过构造函数传递依赖
- Context 必须作为第一个参数,并在所有层中传递
数据库¶
- 所有查询均以纯 SQL 编写于
queries/— 由 sqlc 生成类型安全的 Go 代码 - 使用 golang-migrate 将迁移放在
migrations/— 不要直接修改数据库 - 多步骤操作使用
pgx.Tx事务 - 所有查询使用参数化占位符(
$1、$2)— 禁止使用字符串格式化
错误处理¶
- 返回错误,不要 panic — panic 仅用于真正无法恢复的情况
- 结合上下文包装错误:
fmt.Errorf("creating user: %w", err) - 用于业务逻辑的 sentinel 错误定义在
domain/errors.go中 - 在 handler 层将领域错误映射为 gRPC 状态码
// 도메인 레이어 — sentinel 오류
var (
ErrUserNotFound = errors.New("user not found")
ErrEmailTaken = errors.New("email already registered")
)
// Handler 레이어 — gRPC status로 매핑
func toGRPCError(err error) error {
switch {
case errors.Is(err, domain.ErrUserNotFound):
return status.Error(codes.NotFound, err.Error())
case errors.Is(err, domain.ErrEmailTaken):
return status.Error(codes.AlreadyExists, err.Error())
default:
return status.Error(codes.Internal, "internal error")
}
}
代码风格¶
- 禁止在代码或注释中使用表情符号
- 对外公开的类型和函数必须编写 doc 注释
- 函数保持在 50 行以内 — 拆分出辅助函数
- 对所有有多个用例的逻辑使用 table-driven 测试
- signal 通道使用
struct{}而非bool
文件结构¶
cmd/
server/
main.go # 진입점, Wire 주입, 우아한 종료
internal/
domain/ # 비즈니스 타입과 인터페이스
user.go # User 엔티티와 repository 인터페이스
errors.go # Sentinel 오류
service/ # 비즈니스 로직
user_service.go
user_service_test.go
repository/ # 데이터 접근 (sqlc 생성 + 커스텀)
postgres/
user_repo.go
user_repo_test.go # testcontainers를 사용한 통합 테스트
handler/ # gRPC + REST 핸들러
grpc/
user_handler.go
rest/
user_handler.go
config/ # 설정 로딩
config.go
proto/ # Protobuf 정의
user/v1/
user.proto
queries/ # sqlc용 SQL 쿼리
user.sql
migrations/ # 데이터베이스 마이그레이션
001_create_users.up.sql
001_create_users.down.sql
主要模式¶
Repository 接口¶
type UserRepository interface {
Create(ctx context.Context, user *User) error
FindByID(ctx context.Context, id uuid.UUID) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id uuid.UUID) error
}
使用依赖注入的 Service¶
type UserService struct {
repo domain.UserRepository
hasher PasswordHasher
logger *slog.Logger
}
func NewUserService(repo domain.UserRepository, hasher PasswordHasher, logger *slog.Logger) *UserService {
return &UserService{repo: repo, hasher: hasher, logger: logger}
}
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*domain.User, error) {
existing, err := s.repo.FindByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, domain.ErrUserNotFound) {
return nil, fmt.Errorf("checking email: %w", err)
}
if existing != nil {
return nil, domain.ErrEmailTaken
}
hashed, err := s.hasher.Hash(req.Password)
if err != nil {
return nil, fmt.Errorf("hashing password: %w", err)
}
user := &domain.User{
ID: uuid.New(),
Name: req.Name,
Email: req.Email,
Password: hashed,
}
if err := s.repo.Create(ctx, user); err != nil {
return nil, fmt.Errorf("creating user: %w", err)
}
return user, nil
}
Table-Driven 测试¶
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
req CreateUserRequest
setup func(*MockUserRepo)
wantErr error
}{
{
name: "valid user",
req: CreateUserRequest{Name: "Alice", Email: "alice@example.com", Password: "secure123"},
setup: func(m *MockUserRepo) {
m.On("FindByEmail", mock.Anything, "alice@example.com").Return(nil, domain.ErrUserNotFound)
m.On("Create", mock.Anything, mock.Anything).Return(nil)
},
wantErr: nil,
},
{
name: "duplicate email",
req: CreateUserRequest{Name: "Alice", Email: "taken@example.com", Password: "secure123"},
setup: func(m *MockUserRepo) {
m.On("FindByEmail", mock.Anything, "taken@example.com").Return(&domain.User{}, nil)
},
wantErr: domain.ErrEmailTaken,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := new(MockUserRepo)
tt.setup(repo)
svc := NewUserService(repo, &bcryptHasher{}, slog.Default())
_, err := svc.Create(context.Background(), tt.req)
if tt.wantErr != nil {
assert.ErrorIs(t, err, tt.wantErr)
} else {
assert.NoError(t, err)
}
})
}
}
环境变量¶
# 데이터베이스
DATABASE_URL=postgres://user:pass@localhost:5432/myservice?sslmode=disable
# gRPC
GRPC_PORT=50051
REST_PORT=8080
# 인증
JWT_SECRET= # 프로덕션에서는 vault에서 로드
TOKEN_EXPIRY=24h
# 관측 가능성
LOG_LEVEL=info # debug, info, warn, error
OTEL_ENDPOINT= # OpenTelemetry 콜렉터
测试策略¶
测试命令¶
# 단위 테스트 (빠름, 외부 의존성 없음)
go test ./internal/... -short -count=1
# 통합 테스트 (testcontainers를 위해 Docker 필요)
go test ./internal/repository/... -count=1 -timeout 120s
# 전체 테스트와 커버리지
go test ./... -coverprofile=coverage.out -count=1
go tool cover -func=coverage.out # 요약
go tool cover -html=coverage.out # 브라우저
# Race detector
go test ./... -race -count=1
ECC 工作流¶
# 계획 수립
/plan "Add rate limiting to user endpoints"
# 개발
/go-test # Go 전용 패턴으로 TDD
# 리뷰
/go-review # Go 관용구, 오류 처리, 동시성
/security-scan # 시크릿 및 취약점 점검
# 머지 전 확인
go vet ./...
staticcheck ./...
Git 工作流¶
feat:新功能,fix:Bug 修复,refactor:代码变更- 从
main创建 feature 分支,必须 PR - CI:
go vet、staticcheck、go test -race、golangci-lint - 部署:在 CI 中构建 Docker 镜像,部署到 Kubernetes
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 ev/ECC