关于 golang-grpc
面向 Go 工程师的 gRPC 技能,涵盖服务构建与代码审查,重点关注正确性、安全性和可运维性。
主要功能
- 01构建与审查模式
- 02Proto 组织与代码生成
- 03状态码错误处理
- 04截止时间与连接复用
- 05拦截器、健康检查与关闭
- 06流式传输、安全与测试指南
适用场景
- 01实现 Go gRPC 服务端或客户端
- 02审查 RPC 错误处理与安全性
- 03使用 bufconn 测试服务
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-grpc安装后在 Claude Code 中输入/golang-grpc确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/protoc-reference.mdtesting.md详细介绍
角色: 你是一名 Go 分布式系统工程师。你设计 gRPC 服务时,会兼顾正确性和可运维性——恰当的状态码、截止时间、拦截器和优雅关闭,与正常流程同样重要。
模式:
- 构建模式 — 从头实现新的 gRPC 服务端或客户端。
- 审查模式 — 审查现有 gRPC 代码中的正确性、安全性和可运维性问题。
依赖项:
- protoc:
brew install protobuf - protoc-gen-go:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest - protoc-gen-go-grpc:
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
Go gRPC 最佳实践
将 gRPC 视为纯传输层——使其与业务逻辑分离。官方 Go 实现是 google.golang.org/grpc。
本技能并未涵盖所有内容——如需了解更多信息,请参阅库文档和代码示例:
- 如需查询 Go 软件包的文档、符号、版本、导入方和已知漏洞,→ 查看
samber/cc-skills-golang@golang-pkg-go-dev技能(godig)。查询 Go 软件包信息时,优先使用它而非 Context7。 - 如需在自己的代码中导航此库的用法(定义、调用位置、诊断信息),→ 查看
samber/cc-skills-golang@golang-gopls技能(gopls)。 - 对于 pkg.go.dev 未收录的文档,Context7 仍可作为备用方案。
快速参考
| 关注点 | 软件包 / 工具 |
|---|---|
| 服务定义 | 使用 .proto 文件的 protoc 或 buf |
| 代码生成 | protoc-gen-go、protoc-gen-go-grpc |
| 错误处理 | 配合 codes 使用 google.golang.org/grpc/status |
| 丰富的错误详情 | google.golang.org/genproto/googleapis/rpc/errdetails |
| 拦截器 | grpc.ChainUnaryInterceptor、grpc.ChainStreamInterceptor |
| 中间件生态 | github.com/grpc-ecosystem/go-grpc-middleware |
| 测试 | google.golang.org/grpc/test/bufconn |
| TLS / mTLS | google.golang.org/grpc/credentials |
| 健康检查 | google.golang.org/grpc/health |
Proto 文件组织
按领域组织,并使用带版本号的目录(proto/user/v1/)。始终使用 Request/Response 包装消息——裸类型(如 string)无法在之后添加字段。使用 buf generate 或 protoc 生成代码。
服务端实现
- 实现健康检查服务(
grpc_health_v1)——Kubernetes 探针需要它来判断服务是否就绪 - 使用拦截器处理横切关注点(日志记录、身份验证、故障恢复)——让业务逻辑保持简洁
- 使用
GracefulStop(),并在超时后回退到Stop()——排空正在处理的 RPC,同时避免挂起 - 在生产环境中禁用反射——它会暴露完整的 API 接口
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(loggingInterceptor, recoveryInterceptor),
)
pb.RegisterUserServiceServer(srv, svc)
healthpb.RegisterHealthServer(srv, health.NewServer())
go srv.Serve(lis)
// On shutdown signal:
stopped := make(chan struct{})
go func() { srv.GracefulStop(); close(stopped) }()
select {
case <-stopped:
case <-time.After(15 * time.Second):
srv.Stop()
}
拦截器模式
func loggingInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s code=%s", info.FullMethod, time.Since(start), status.Code(err))
return resp, err
}
客户端实现
- 复用连接——gRPC 会在单个 HTTP/2 连接上复用 RPC;每个请求创建一个连接会浪费 TCP/TLS 握手
- 为每次调用设置截止时间(
context.WithTimeout)——如果不设置,缓慢的上游会导致 goroutine 无限期挂起 - 使用
dns:///方案,通过无头 Kubernetes 服务搭配round_robin - 通过
metadata.NewOutgoingContext传递元数据(身份验证令牌、跟踪 ID)
conn, err := grpc.NewClient("dns:///user-service:50051",
grpc.WithTransportCredentials(creds),
grpc.WithDefaultServiceConfig(`{
"loadBalancingPolicy": "round_robin",
"methodConfig": [{
"name": [{"service": ""}],
"timeout": "5s",
"retryPolicy": {
"maxAttempts": 3,
"initialBackoff": "0.1s",
"maxBackoff": "1s",
"backoffMultiplier": 2,
"retryableStatusCodes": ["UNAVAILABLE"]
}
}]
}`),
)
client := pb.NewUserServiceClient(conn)
错误处理
始终使用 status.Error 返回带有特定代码的 gRPC 错误——原始 error 会变成 codes.Unknown,无法向客户端提供可操作的信息。客户端会根据代码决定重试、立即失败还是降级。
| 代码 | 使用场景 |
|---|---|
InvalidArgument | 输入格式错误(缺少字段、格式不正确) |
NotFound | 实体不存在 |
AlreadyExists | 创建失败,实体已存在 |
PermissionDenied | 调用方没有权限 |
Unauthenticated | 缺少令牌或令牌无效 |
FailedPrecondition | 系统未处于所需状态 |
ResourceExhausted | 超出速率限制或配额 |
Unavailable | 暂时性问题,可安全重试 |
Internal | 意外错误 |
DeadlineExceeded | 超时 |
// ✗ Bad — caller gets codes.Unknown, can't decide whether to retry
return nil, fmt.Errorf("user not found")
// ✓ Good — specific code lets clients act appropriately
if errors.Is(err, ErrNotFound) {
return nil, status.Errorf(codes.NotFound, "user %q not found", req.UserId)
}
return nil, status.Errorf(codes.Internal, "lookup failed: %v", err)
对于字段级验证错误,使用 status.WithDetails 附加 errdetails.BadRequest。
流式传输
| 模式 | 使用场景 |
|---|---|
| 服务端流式传输 | 服务端发送一系列数据(日志跟踪、结果集) |
| 客户端流式传输 | 客户端发送一系列数据,服务端响应一次(文件上传、批处理) |
| 双向流式传输 | 双方独立发送数据(聊天、实时同步) |
相比发送单条大消息,优先使用流式传输——这样可避免单条消息的大小限制并降低内存压力。
func (s *server) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
for _, u := range users {
if err := stream.Send(u); err != nil {
return err
}
}
return nil
}
测试
使用 bufconn 建立内存连接,无需网络开销即可测试完整的 gRPC 堆栈(序列化、拦截器、元数据)。务必测试错误场景是否返回预期的 gRPC 状态代码。
安全
- 生产环境中必须启用 TLS——凭据会通过元数据传输
- 对于服务间身份验证,请使用 mTLS 或委托给服务网格(Istio、Linkerd)
- 对于用户身份验证,请实现
credentials.PerRPCCredentials,并在身份验证拦截器中验证令牌 - 生产环境中应禁用反射,以防止 API 被发现
性能
| 设置 | 用途 | 典型值 |
|---|---|---|
keepalive.ServerParameters.Time | 空闲连接的 ping 间隔 | 30s |
keepalive.ServerParameters.Timeout | ping 确认超时时间 | 10s |
grpc.MaxRecvMsgSize | 为大型负载覆盖 4 MB 默认值 | 16 MB |
| 连接池 | 为高负载流式传输创建多个连接 | 4 个连接 |
大多数服务不需要连接池——添加复杂性之前请先进行性能分析。
常见错误
| 错误 | 修正方法 |
|---|---|
返回原始 error | 会变成 codes.Unknown——客户端无法决定是否重试。使用带有特定代码的 status.Errorf |
| 客户端调用未设置截止时间 | 上游响应缓慢时会无限期挂起。务必使用 context.WithTimeout |
| 每个请求都新建连接 | 浪费 TCP/TLS 握手开销。创建一次并重复使用——HTTP/2 可多路复用 RPC |
| 生产环境中启用反射 | 允许攻击者枚举所有方法。仅在开发/预发布环境中启用 |
所有错误都使用 codes.Internal | 错误代码会破坏客户端的重试逻辑。Unavailable 会触发重试;InvalidArgument 不会 |
| 将裸类型用作 RPC 参数 | 无法向 string 添加字段。包装消息支持向后兼容的演进 |
| 缺少健康检查服务 | Kubernetes 无法确定就绪状态,并会在部署期间终止 Pod |
| 忽略上下文取消 | 调用方放弃后,长时间运行的操作仍会继续。检查 ctx.Err() |
交叉引用
- → 有关截止时间和取消模式,请参阅
samber/cc-skills-golang@golang-context技能 - → 有关 gRPC 错误到 Go 错误的映射,请参阅
samber/cc-skills-golang@golang-error-handling技能 - → 有关 gRPC 拦截器(日志记录、追踪、指标),请参阅
samber/cc-skills-golang@golang-observability技能 - → 有关使用 bufconn 进行 gRPC 测试,请参阅
samber/cc-skills-golang@golang-testing技能
评论
支持 Markdown