RPC 服务开发
go-zero 基于 gRPC 和 Protobuf 构建高性能的内部服务通信层,自动集成 P2C 负载均衡、熔断器、服务发现等治理能力。
Protobuf 快速回顾
Protocol Buffers 是 Google 的序列化协议,比 JSON 更小更快:
syntax = "proto3";
package user;
option go_package = "github.com/example/user";
service UserService {
rpc GetUser (GetUserReq) returns (GetUserResp);
rpc CreateUser (CreateUserReq) returns (CreateUserResp);
}
message GetUserReq {
int64 id = 1;
}
message GetUserResp {
int64 id = 1;
string name = 2;
string email = 3;
}
message CreateUserReq {
string name = 1;
string email = 2;
}
message CreateUserResp {
int64 id = 1;
}go-zero RPC 代码生成
从 proto 生成服务
# 方式1:完整生成(推荐)
goctl rpc protoc user.proto --go_out=./ --go-grpc_out=./ --zrpc_out=./
# 方式2:多 service 分组生成
goctl rpc protoc user.proto --go_out=./ --go-grpc_out=./ --zrpc_out=./ -m💡
-m参数表示启用多 service 分组模式,关键区别见下文「服务分组」。
生成的项目结构
user/
├── etc/
│ └── user.yaml # RPC 服务配置
├── internal/
│ ├── config/
│ │ └── config.go # 配置结构体
│ ├── logic/
│ │ └── getuserlogic.go # ← 业务逻辑在这里
│ ├── server/
│ │ └── userserviceserver.go # gRPC 服务端适配器
│ └── svc/
│ └── servicecontext.go # 依赖注入
├── user/ # 或 client/
│ ├── user.pb.go # Protobuf 数据类型
│ └── user_grpc.pb.go # gRPC 客户端/服务端桩
└── user.go # 主入口实现 RPC 业务逻辑
打开 internal/logic/getuserlogic.go:
func (l *GetUserLogic) GetUser(in *user.GetUserReq) (*user.GetUserResp, error) {
// 业务逻辑:查数据库、调用其他 RPC、处理缓存等
return &user.GetUserResp{
Id: in.Id,
Name: "alice",
Email: "alice@example.com",
}, nil
}配置 RPC 服务
etc/user.yaml:
Name: user.rpc
ListenOn: 0.0.0.0:8080
Timeout: 3000 # 毫秒
# 服务注册发现(etcd)
Etcd:
Hosts:
- 127.0.0.1:2379
- 127.0.0.1:2380 # 集群多节点
Key: user.rpc
# 日志
Log:
ServiceName: user.rpc
Mode: file
Path: logs/user.rpc
# 监控
Prometheus:
Host: 0.0.0.0
Port: 9091
Path: /metrics
# 链路追踪
Telemetry:
Name: user.rpc
Endpoint: http://jaeger:14268/api/traces
Sampler: 1.0
Batcher: jaeger配置结构体
// internal/config/config.go
type Config struct {
zrpc.RpcServerConf // 嵌入 RPC 标准配置
// 自定义配置字段...
}RPC 客户端调用
客户端代码生成
goctl 自动生成客户端代码,无分组时在 user/ 目录下,有分组时在 client/ 目录下:
// 客户端代码(自动生成)
type User interface {
GetUser(ctx context.Context, in *GetUserReq, opts ...grpc.CallOption) (*GetUserResp, error)
}
func NewUser(cc grpc.ClientConnInterface) User {
return &defaultUser{cc}
}在 API 服务中使用
Config 中声明:
type Config struct {
rest.RestConf
UserRpcConf zrpc.RpcClientConf
}YAML 配置:
UserRpcConf:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Timeout: 2000 # 毫秒,默认 2000ServiceContext 注入:
type ServiceContext struct {
Config config.Config
UserRpc userclient.User
}
func NewServiceContext(c config.Config) *ServiceContext {
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUser(zrpc.MustNewClient(c.UserRpcConf)),
}
}Logic 中调用:
func (l *MyLogic) DoSomething(req *types.Req) error {
// 设置超时 context
ctx, cancel := context.WithTimeout(l.ctx, 2*time.Second)
defer cancel()
resp, err := l.svcCtx.UserRpc.GetUser(ctx, &user.GetUserReq{Id: req.UserId})
if err != nil {
return err
}
// 使用 resp...
}🚨 必须设置 RPC 超时!gRPC 默认无限等待,不设超时可能导致调用方被挂起。
服务分组(多 Service 模式)
proto 文件中可以定义多个 service,goctl 用 -m 参数启用分组:
syntax = "proto3";
package user;
option go_package = "github.com/example/user";
message LoginReq {}
message LoginResp {}
message UserInfoReq {}
message UserInfoResp {}
service UserService {
rpc Login (LoginReq) returns (LoginResp);
rpc UserInfo (UserInfoReq) returns (UserInfoResp);
}
message UserRoleListReq {}
message UserRoleListResp {}
service UserRoleService {
rpc UserRoleList (UserRoleListReq) returns (UserRoleListResp);
}
message UserClassListReq {}
message UserClassListResp {}
service UserClassService {
rpc UserClassList (UserClassListReq) returns (UserClassListResp);
}生成命令:
goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. -m生成结构(有分组):
.
├── client/ # 客户端代码按 service 分开
│ ├── userservice/
│ │ └── userservice.go
│ ├── userroleservice/
│ │ └── userroleservice.go
│ └── userclassservice/
│ └── userclassservice.go
├── etc/
│ └── user.yaml
├── internal/
│ ├── logic/
│ │ ├── userservice/ # 按 service 分组
│ │ │ ├── loginlogic.go
│ │ │ └── userinfologic.go
│ │ ├── userroleservice/
│ │ │ └── userrolelistlogic.go
│ │ └── userclassservice/
│ │ └── userclasslistlogic.go
│ ├── server/
│ │ ├── userservice/
│ │ ├── userroleservice/
│ │ └── userclassservice/
│ └── svc/
└── user.go🚨 不分组模式下,不支持在 proto 文件中定义多个 service,否则会报错。
gRPC 流式调用
go-zero 支持 gRPC 的四种调用模式:
一元 RPC(Unary)
rpc GetUser(GetUserReq) returns (GetUserResp);最常见的请求-响应模式。
服务端流式(Server Streaming)
rpc ListUsers(ListUsersReq) returns (stream ListUsersResp);func (l *ListUsersLogic) ListUsers(in *user.ListUsersReq, stream user.UserService_ListUsersServer) error {
for i := 0; i < 10; i++ {
stream.Send(&user.ListUsersResp{Id: int64(i), Name: fmt.Sprintf("user-%d", i)})
}
return nil
}客户端流式(Client Streaming)
rpc BatchCreate(stream CreateUserReq) returns (BatchCreateResp);双向流式(Bidirectional Streaming)
rpc Chat(stream ChatMsg) returns (stream ChatMsg);RPC 拦截器
go-zero 支持 Unary 和 Stream 两种拦截器:
// Unary 拦截器
func LoggerInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
start := time.Now()
resp, err := handler(ctx, req)
logx.WithContext(ctx).Infow("RPC call",
logx.Field("method", info.FullMethod),
logx.Field("duration", time.Since(start)),
)
return resp, err
}
// 在 main.go 中注册
s.AddUnaryInterceptors(LoggerInterceptor)💡 中间件/拦截器的完整内容见 11-中间件与拦截器.md。
RPC 超时控制
go-zero 的 gRPC 客户端支持通过 context 控制超时:
// 方式1:在调用方设置
ctx, cancel := context.WithTimeout(l.ctx, 2*time.Second)
defer cancel()
// 方式2:在配置中全局设置
// yaml: Timeout: 2000 (毫秒)🔬 超时值取客户端 context 超时和服务端配置超时的最小值。
RPC 直连模式(跳过服务发现)
当不想依赖 etcd 时,使用直连:
# API 端配置
UserRpc:
Endpoints:
- 127.0.0.1:8080
Target: "" # 留空即直连或使用 K8s DNS:
UserRpc:
Target: dns:///user-svc.default.svc.cluster.local:8080gRPC 健康检查
go-zero 自动实现 gRPC 健康检查协议,可直接对接 K8s 探针:
# K8s Deployment 中
livenessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080
readinessProbe:
exec:
command:
- /bin/grpc_health_probe
- -addr=:8080客户端连接池管理
go-zero 的 zrpc.RpcClientConf 支持连接池配置:
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
Timeout: 2000
# 连接池(zrpc 默认值通常足够)
MaxPoolSize: 10🚨
MaxPoolSize默认值为 10,不可动态调整。服务突发扩容时连接数可能不够,需要监控rpc_client_pool_size指标,必要时调整。
常见陷阱
| 陷阱 | 说明 |
|---|---|
| 🚨 RPC Client 首次调用延迟 2-3 秒 | zrpc 客户端懒加载,首次调用才建连。建议在 main 函数中预热:client.Ping(ctx) |
| 🚨 每个 logic 里 new 一个 client | client 实例应作为全局变量注入 ServiceContext,否则反复创建连接池 |
| 🚨 RPC 超时未设置 | gRPC 默认无限等待,必须用 context.WithTimeout |
| 🚨 连接池耗尽无错误日志 | 高并发下 MaxPoolSize 不够时会静默丢弃请求 |
🚨 proto 文件多 service 不加 -m |
不分组模式不支持多 service,会报错 |
| 🚨 etcd 未启动但配置了 Etcd | RPC 服务启动失败。不依赖 etcd 时删除 Etcd 配置块即可 |
| 🚨 循环 RPC 调用 | A 调 B,B 调 C,C 又调 A。严禁循环调用!用 core/mr 包将串行改并行 |