Appearance
RPC 服务开发
本篇深入讲解 go-zero 的 RPC 服务开发。我们将基于 gRPC 介绍 .proto 文件编写、goctl rpc 代码生成、服务端实现、客户端调用,重点演示 API 服务如何调用 RPC 服务(这是微服务架构中最常见的协作模式),最后通过一个完整的「用户 API + 用户 RPC」示例把所有环节串起来。
一、go-zero RPC 基础
go-zero 的 RPC 服务基于 gRPC 构建,并在此基础上做了大量工程化封装:
- 服务注册与发现:默认集成 etcd,自动注册服务实例
- 客户端负载均衡:内置 p2c(Power of Two Choices)算法
- 自适应熔断:基于 Google SRE 算法,根据成功率自动降级
- 链路追踪:自动注入 TraceID,跨服务传播
- 代码生成:goctl 从
.proto一键生成 server/client/logic 骨架
go-zero RPC 的核心包是 zrpc,它对 gRPC 的 grpc.Server 和 grpc.ClientConn 做了封装,统一了配置、拦截器、监控等能力。
1. gRPC 与 zrpc 的关系
| 维度 | 原生 gRPC | go-zero zrpc |
|---|---|---|
| 服务注册 | 需自己实现 | 内置 etcd |
| 负载均衡 | 需配置 resolver | 内置 p2c |
| 熔断 | 需引入第三方 | 内置 googlebreaker |
| 拦截器 | 手写链 | 内置链式 |
| 监控 | 手写 | 内置 Prometheus |
| 配置 | 手写 | YAML + struct |
简单说:zrpc = gRPC + 工程化封装。原生 gRPC 能做的 zrpc 都能做,且开箱即用。
二、.proto 文件编写
.proto 是 Protocol Buffers 的接口描述语言,gRPC 用它来定义服务契约。go-zero 完全兼容标准 protobuf 语法。
1. 基本结构
proto
syntax = "proto3";
package user;
option go_package = "./user";
// 用户信息
message UserInfo {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
UserInfo user = 1;
}
service User {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}关键字段:
syntax = "proto3":使用 proto3 语法(推荐)package:包名,避免命名冲突option go_package:生成 Go 代码的导入路径message:定义数据结构service:定义 RPC 服务和方法
2. 字段类型映射
protobuf 类型与 Go 类型映射:
| protobuf | Go |
|---|---|
int32 / int64 | int32 / int64 |
uint32 / uint64 | uint32 / uint64 |
string | string |
bool | bool |
float / double | float32 / float64 |
bytes | []byte |
repeated T | []T |
map<K,V> | map[K]V |
3. 流式接口
gRPC 支持三种调用模式:
proto
service Chat {
// 一元调用(最常用)
rpc SendMessage(ChatRequest) returns (ChatResponse);
// 服务端流
rpc Subscribe(SubscribeRequest) returns (stream Event);
// 客户端流
rpc Upload(stream Chunk) returns (UploadResponse);
// 双向流
rpc ChatStream(stream Message) returns (stream Message);
}go-zero 对流式接口的支持与原生 gRPC 一致,goctl 会生成对应的 stub。
4. 完整的 user.proto
proto
syntax = "proto3";
package user;
option go_package = "./user";
message UserInfo {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
int64 createTime = 5;
int64 updateTime = 6;
}
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
UserInfo user = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
string password = 4;
}
message CreateUserResponse {
int64 id = 1;
}
message ListUserRequest {
string keyword = 1;
int32 page = 2;
int32 pageSize = 3;
}
message ListUserResponse {
int64 total = 1;
repeated UserInfo list = 2;
}
service User {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc ListUser(ListUserRequest) returns (ListUserResponse);
}三、goctl rpc proto 生成代码
1. 生成命令
bash
goctl rpc protoc user.proto \
--go_out=./pb \
--go-grpc_out=./pb \
--zrpc_out=. \
--style=goZero参数说明:
| 参数 | 说明 |
|---|---|
--go_out | protoc-gen-go 生成的 .pb.go 输出目录 |
--go-grpc_out | protoc-gen-go-grpc 生成的 _grpc.pb.go 输出目录 |
--zrpc_out | go-zero RPC 代码(server/client/logic)输出目录 |
--style | 命名风格,goZero/go_zero/gozero |
--home | 自定义模板目录 |
注意:
--go_out和--go-grpc_out实际是传给protoc的,goctl 内部会调用protoc命令。
2. 生成后的目录结构
user-rpc/
├── etc
│ └── user.yaml # 服务端配置
├── go.mod
├── user.proto
├── pb/ # protobuf 生成代码
│ ├── user.pb.go
│ └── user_grpc.pb.go
├── user.go # main 入口
├── userclient/ # 客户端封装
│ └── user.go
└── internal/
├── config
│ └── config.go # 服务端配置结构
├── logic/ # 业务逻辑(重点编辑)
│ ├── getuserlogic.go
│ ├── createuserlogic.go
│ └── listuserlogic.go
├── server/
│ └── userserver.go # gRPC service 实现
├── svc/
│ └── servicecontext.go # 依赖注入
└── scheduler/ # 可选,定时任务3. main 入口
go
// user.go
package main
import (
"flag"
"fmt"
"user-rpc/internal/config"
"user-rpc/internal/server"
"user-rpc/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/zrpc"
)
var configFile = flag.String("f", "etc/user.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
ctx := svc.NewServiceContext(c)
srv := server.NewUserServer(ctx)
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
user.RegisterUserServer(grpcServer, srv)
})
defer s.Stop()
fmt.Printf("Starting rpc server at %s...\n", c.ListenOn)
s.Start()
}四、RPC 服务端实现
1. Server 配置
internal/config/config.go:
go
package config
import "github.com/zeromicro/go-zero/zrpc"
type Config struct {
zrpc.RpcServerConf
}etc/user.yaml:
yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
# etcd 服务注册(生产环境必填)
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
# 监控(Prometheus)
Prometheus:
Host: 0.0.0.0
Port: 9101
Path: /metrics
# 链路追踪(Jaeger)
Telemetry:
Name: user.rpc
Endpoint: http://127.0.0.1:14268/api/traces
Sampler: 1.0
Batcher: jaeger配置项说明:
ListenOn:服务监听地址Etcd:服务注册配置,多个实例注册到同一个Key下,客户端通过 Key 发现所有实例Prometheus:监控指标暴露端口Telemetry:链路追踪配置
如果本地没有 etcd,可以注释掉
Etcd块,服务以「直连模式」启动,客户端通过Endpoints直连。
2. Server 实现
internal/server/userserver.go(goctl 生成,一般不改):
go
package server
import (
"context"
"user-rpc/internal/logic"
"user-rpc/internal/svc"
"user-rpc/pb"
)
type UserServer struct {
ctx *svc.ServiceContext
pb.UnimplementedUserServer
}
func NewUserServer(ctx *svc.ServiceContext) *UserServer {
return &UserServer{ctx: ctx}
}
func (s *UserServer) GetUser(ctx context.Context, in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
l := logic.NewGetUserLogic(ctx, s.ctx)
return l.GetUser(in)
}
func (s *UserServer) CreateUser(ctx context.Context, in *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
l := logic.NewCreateUserLogic(ctx, s.ctx)
return l.CreateUser(in)
}
func (s *UserServer) ListUser(ctx context.Context, in *pb.ListUserRequest) (*pb.ListUserResponse, error) {
l := logic.NewListUserLogic(ctx, s.ctx)
return l.ListUser(in)
}可以看到 Server 层只是把请求转发给对应的 Logic,与 API 的 Handler 角色一致。
3. Logic 层业务实现
internal/logic/getuserlogic.go:
go
package logic
import (
"context"
"fmt"
"time"
"user-rpc/internal/svc"
"user-rpc/pb"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *GetUserLogic) GetUser(in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
l.Infof("get user, id=%d", in.Id)
if in.Id <= 0 {
return nil, fmt.Errorf("invalid user id: %d", in.Id)
}
// 从 DB 查询(这里用 mock)
user, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
l.Errorf("query user failed: %v", err)
return nil, err
}
if user == nil {
return nil, fmt.Errorf("user not found")
}
return &pb.GetUserResponse{
User: &pb.UserInfo{
Id: user.Id,
Name: user.Name,
Email: user.Email,
Age: user.Age,
CreateTime: user.CreateTime.Unix(),
UpdateTime: user.UpdateTime.Unix(),
},
}, nil
}4. ServiceContext 与依赖注入
go
// internal/svc/servicecontext.go
package svc
import (
"github.com/zeromicro/go-zero/core/stores/sqlx"
"user-rpc/internal/config"
"user-rpc/internal/model"
)
type ServiceContext struct {
Config config.Config
UserModel model.UserModel
}
func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.MySQL.DataSource)
return &ServiceContext{
Config: c,
UserModel: model.NewUserModel(conn, c.CacheRedis),
}
}配置结构需要扩展:
go
// internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
zrpc.RpcServerConf
MySQL struct {
DataSource string
}
CacheRedis cache.ClusterConf
}5. 服务注册(etcd)
go-zero 默认通过 etcd 注册服务。启动时,服务实例把自己的 ListenOn 地址注册到 etcd 的 Etcd.Key 下,客户端通过该 Key 拿到所有实例地址,再用 p2c 负载均衡选择一个调用。
yaml
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc多实例部署时,所有实例都用同一个 Key,etcd 会维护一个实例列表。某个实例宕机时,etcd 会自动剔除(基于 lease 机制)。
6. 运行服务
bash
go mod tidy
go run user.go -f etc/user.yaml五、RPC 客户端实现
1. Client 配置
goctl 生成的 userclient/user.go 已经封装好了客户端:
go
// userclient/user.go(goctl 生成)
package userclient
import (
"user-rpc/pb"
"github.com/zeromicro/go-zero/zrpc"
)
type UserClient interface {
GetUser(in *pb.GetUserRequest, opts ...grpc.CallOption) (*pb.GetUserResponse, error)
CreateUser(in *pb.CreateUserRequest, opts ...grpc.CallOption) (*pb.CreateUserResponse, error)
ListUser(in *pb.ListUserRequest, opts ...grpc.CallOption) (*pb.ListUserResponse, error)
}
type userClient struct {
client pb.UserClient
}
func NewUserClient(cli zrpc.Client) UserClient {
return &userClient{
client: pb.NewUserClient(cli.Conn()),
}
}
func (c *userClient) GetUser(in *pb.GetUserRequest, opts ...grpc.CallOption) (*pb.GetUserResponse, error) {
return c.client.GetUser(context.Background(), in, opts...)
}
// ... 其他方法类似2. 创建 Client
直连模式
不依赖 etcd,直接指定目标地址:
go
package main
import (
"context"
"fmt"
"time"
"user-rpc/pb"
"user-rpc/userclient"
"github.com/zeromicro/go-zero/zrpc"
)
func main() {
client, err := zrpc.NewClientWithTarget("127.0.0.1:8080")
if err != nil {
panic(err)
}
cli := userclient.NewUserClient(client)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
resp, err := cli.GetUser(ctx, &pb.GetUserRequest{Id: 1})
if err != nil {
panic(err)
}
fmt.Printf("user: %+v\n", resp.User)
}etcd 模式(推荐生产环境)
通过 etcd 服务发现:
go
client, err := zrpc.NewClient(zrpc.RpcClientConf{
Etcd: discov.EtcdConf{
Hosts: []string{"127.0.0.1:2379"},
Key: "user.rpc",
},
NonBlock: true,
Timeout: 2000, // ms
})
if err != nil {
panic(err)
}
cli := userclient.NewUserClient(client)对应的配置文件形式:
yaml
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
NonBlock: true
Timeout: 20003. 负载均衡与重试
go-zero 客户端默认使用 p2c 负载均衡算法。p2c(Power of Two Choices)会随机选两个实例,再选负载较低的那个,比简单轮询更智能,能避免把请求打到最繁忙的实例上。
p2c 的核心思想:
- 随机选两个后端实例
- 比较两者的负载(连接数、延迟、成功率等综合指标)
- 选择负载较低的那个
这种算法实现简单,但在大规模集群下表现优于轮询,能有效避免「热点实例」问题。
重试机制通过 grpc.CallOption 控制:
go
import "google.golang.org/grpc"
resp, err := cli.GetUser(ctx, &pb.GetUserRequest{Id: 1},
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(10*1024*1024),
),
)go-zero 内置了自适应熔断:当某个实例的请求失败率超过阈值时,客户端会自动「拉黑」该实例一段时间,避免雪崩。这个机制是透明的,开发者无需关心。
4. 超时控制
客户端的 Timeout 配置(毫秒)会作为默认超时。也可以在每次调用时用 context.WithTimeout 控制:
go
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
resp, err := cli.GetUser(ctx, &pb.GetUserRequest{Id: 1})六、API 调用 RPC
这是微服务架构中最常见的协作模式:HTTP 请求进入 API 服务,API 服务调用 RPC 服务获取数据,再返回给客户端。go-zero 让这个链路非常顺滑。
1. 在 API 服务的 SVC 中注入 RPC Client
API 服务的配置结构扩展:
go
// user-api/internal/config/config.go
package config
import (
"github.com/zeromicro/go-zero/rest"
"github.com/zeromicro/go-zero/zrpc"
)
type Config struct {
rest.RestConf
Auth struct {
AccessSecret string
AccessExpire int64
}
UserRpc zrpc.RpcClientConf
}API 服务的配置文件 etc/user-api.yaml:
yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
Auth:
AccessSecret: "your-secret-key"
AccessExpire: 86400
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
NonBlock: true
Timeout: 2000ServiceContext 注入 RPC Client:
go
// user-api/internal/svc/servicecontext.go
package svc
import (
"github.com/zeromicro/go-zero/zrpc"
"user-api/internal/config"
"user-rpc/userclient" // 引入 RPC 服务的 client 包
)
type ServiceContext struct {
Config config.Config
UserRpc userclient.UserClient
}
func NewServiceContext(c config.Config) *ServiceContext {
rpcClient := zrpc.MustNewClient(c.UserRpc)
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUserClient(rpcClient),
}
}注意:API 服务需要把
user-rpc作为依赖引入到go.mod,通常用replace指向本地路径或仓库地址:go// go.mod require user-rpc v0.0.0 replace user-rpc => ../user-rpc
2. 在 Logic 中调用 RPC
go
// user-api/internal/logic/user/getuserlogic.go
package user
import (
"context"
"strconv"
"user-api/internal/svc"
"user-api/internal/types"
"user-rpc/pb"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
id, err := strconv.ParseInt(req.Id, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid user id: %s", req.Id)
}
// 调用 RPC 服务
rpcResp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
if err != nil {
l.Errorf("call user rpc failed: %v", err)
return nil, err
}
// 组装 API 响应
return &types.GetUserResponse{
User: types.UserVO{
Id: rpcResp.User.Id,
Name: rpcResp.User.Name,
Email: rpcResp.User.Email,
Age: int(rpcResp.User.Age),
CreatedAt: time.Unix(rpcResp.User.CreateTime, 0).Format("2006-01-02 15:04:05"),
},
}, nil
}调用 RPC 时需要注意:
- 直接传
l.ctx,go-zero 会自动传播 TraceID,实现链路追踪 - RPC 调用失败时,错误会带 gRPC status code,可以在 API 层转换成 HTTP 错误码
- 不要在循环里调用 RPC(N+1 问题),应批量查询
3. 错误转换
gRPC 的错误是 status.Status,需要转成 HTTP 错误响应:
go
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
rpcResp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
if err != nil {
// 解析 gRPC 错误码
st, ok := status.FromError(err)
if ok {
switch st.Code() {
case codes.NotFound:
return nil, fmt.Errorf("user not found")
case codes.InvalidArgument:
return nil, fmt.Errorf("invalid argument: %s", st.Message())
default:
return nil, fmt.Errorf("rpc error: %s", st.Message())
}
}
return nil, err
}
// ...
}更优雅的做法是在 RPC 服务端用 status.Error 返回带语义的错误:
go
// user-rpc/internal/logic/getuserlogic.go
import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (l *GetUserLogic) GetUser(in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
if in.Id <= 0 {
return nil, status.Error(codes.InvalidArgument, "invalid user id")
}
user, err := l.svcCtx.UserModel.FindOne(l.ctx, in.Id)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
if user == nil {
return nil, status.Error(codes.NotFound, "user not found")
}
// ...
}七、完整示例:用户 API 调用用户 RPC
下面把前面的所有片段整合成一个完整的可运行示例。
1. 目录结构
demo/
├── user-rpc/ # RPC 服务(数据层)
│ ├── etc/user.yaml
│ ├── user.proto
│ ├── user.go
│ ├── pb/
│ ├── userclient/
│ └── internal/
│ ├── config/
│ ├── logic/
│ ├── server/
│ └── svc/
└── user-api/ # API 服务(HTTP 入口)
├── etc/user-api.yaml
├── user.api
├── user.go
└── internal/
├── config/
├── handler/
├── logic/
├── svc/
└── types/2. user-rpc 实现
user.proto
proto
syntax = "proto3";
package user;
option go_package = "./user";
message UserInfo {
int64 id = 1;
string name = 2;
string email = 3;
int32 age = 4;
int64 createTime = 5;
}
message GetUserRequest { int64 id = 1; }
message GetUserResponse { UserInfo user = 1; }
message CreateUserRequest {
string name = 1;
string email = 2;
int32 age = 3;
string password = 4;
}
message CreateUserResponse { int64 id = 1; }
service User {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
}etc/user.yaml
yaml
Name: user.rpc
ListenOn: 0.0.0.0:8080
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpclogic/getuserlogic.go(核心)
go
package logic
import (
"context"
"user-rpc/internal/svc"
"user-rpc/pb"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *GetUserLogic) GetUser(in *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 这里演示 mock 数据
return &pb.GetUserResponse{
User: &pb.UserInfo{
Id: in.Id,
Name: "alice",
Email: "alice@example.com",
Age: 25,
CreateTime: 1700000000,
},
}, nil
}3. user-api 实现
user.api
api
syntax = "v1"
type (
GetUserRequest {
Id string `path:"id"`
}
GetUserResponse {
Id int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
CreatedAt string `json:"createdAt"`
}
)
@server (
group: user
prefix: /api/v1
)
service user-api {
@handler GetUserHandler
get /user/:id (GetUserRequest) returns (GetUserResponse)
}etc/user-api.yaml
yaml
Name: user-api
Host: 0.0.0.0
Port: 8888
UserRpc:
Etcd:
Hosts:
- 127.0.0.1:2379
Key: user.rpc
NonBlock: true
Timeout: 2000go.mod(API 引用 RPC)
go
module user-api
go 1.20
require (
github.com/zeromicro/go-zero v1.6.0
user-rpc v0.0.0
google.golang.org/grpc v1.58.0
)
replace user-rpc => ../user-rpcServiceContext
go
package svc
import (
"github.com/zeromicro/go-zero/zrpc"
"user-api/internal/config"
"user-rpc/userclient"
)
type ServiceContext struct {
Config config.Config
UserRpc userclient.UserClient
}
func NewServiceContext(c config.Config) *ServiceContext {
rpcClient := zrpc.MustNewClient(c.UserRpc)
return &ServiceContext{
Config: c,
UserRpc: userclient.NewUserClient(rpcClient),
}
}GetUserLogic
go
package user
import (
"context"
"fmt"
"strconv"
"time"
"user-api/internal/svc"
"user-api/internal/types"
"user-rpc/pb"
"github.com/zeromicro/go-zero/core/logx"
)
type GetUserLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewGetUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserLogic {
return &GetUserLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (resp *types.GetUserResponse, err error) {
id, err := strconv.ParseInt(req.Id, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid user id: %s", req.Id)
}
rpcResp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &pb.GetUserRequest{Id: id})
if err != nil {
l.Errorf("call user rpc failed, id=%d, err=%v", id, err)
return nil, err
}
return &types.GetUserResponse{
Id: rpcResp.User.Id,
Name: rpcResp.User.Name,
Email: rpcResp.User.Email,
Age: int(rpcResp.User.Age),
CreatedAt: time.Unix(rpcResp.User.CreateTime, 0).Format("2006-01-02 15:04:05"),
}, nil
}4. 启动与测试
先启动 etcd:
bash
docker run -d --name etcd -p 2379:2379 \
-e ALLOW_NONE_AUTHENTICATION=yes \
bitnami/etcd:3.5启动 RPC 服务:
bash
cd user-rpc
go mod tidy
go run user.go -f etc/user.yaml
# 看到:Starting rpc server at 0.0.0.0:8080...启动 API 服务:
bash
cd user-api
go mod tidy
go run user.go -f etc/user-api.yaml
# 看到:Starting server at 0.0.0.0:8888...测试:
bash
curl http://localhost:8888/api/v1/user/1
# 响应:{"id":1,"name":"alice","email":"alice@example.com","age":25,"createdAt":"2023-11-14 22:13:20"}八、常见问题
1. connection refused / dial tcp
通常是 RPC 服务没启动,或 etcd 地址配置错误。检查:
- etcd 是否运行:
etcdctl --endpoints=127.0.0.1:2379 get user.rpc - RPC 服务是否注册成功
- API 配置中的
Etcd.Key与 RPC 配置中的Etcd.Key是否一致
2. rpc error: code = Unavailable
客户端找不到可用实例。可能原因:
- etcd 中没有对应 Key(RPC 没注册成功)
- 网络隔离(容器网络、防火墙)
NonBlock: true时客户端启动不等待服务,调用时才报错
3. 调用超时
调整 Timeout 配置(毫秒),或在调用时用 context.WithTimeout。
4. proto 字段命名风格
protobuf 默认用 snake_case,生成 Go 代码时自动转 CamelCase。JSON 序列化默认用 camelCase(除非用 json_name 指定)。如果 API 对外要 snake_case,可以在 .api 文件里手动控制 types 的 json tag。
九、小结
本篇系统讲解了 go-zero RPC 服务开发。要点回顾:
.proto定义服务契约,goctl 从中生成 server/client/logic 骨架- 服务端通过
zrpc.MustNewServer启动,Logic 层是业务核心 - 客户端通过
zrpc.NewClient创建,内置 p2c 负载均衡和自适应熔断 - 服务注册默认走 etcd,多实例注册到同一 Key 下
- API 调用 RPC 的标准模式:在 SVC 注入 Client → 在 Logic 调用 → 转换错误码
- 直接传
l.ctx即可自动传播 TraceID,实现链路追踪 - 错误用
status.Error携带语义化错误码,便于 API 层转换
下一篇我们将进入中间件与拦截器,讲解 API 中间件、RPC 拦截器的编写与常用实现(JWT、日志、限流、CORS)。