Skip to content
Go
RPC 服务开发

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   # 毫秒,默认 2000

ServiceContext 注入

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:8080

gRPC 健康检查

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 包将串行改并行