Skip to content

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.Servergrpc.ClientConn 做了封装,统一了配置、拦截器、监控等能力。

1. gRPC 与 zrpc 的关系

维度原生 gRPCgo-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 类型映射:

protobufGo
int32 / int64int32 / int64
uint32 / uint64uint32 / uint64
stringstring
boolbool
float / doublefloat32 / 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_outprotoc-gen-go 生成的 .pb.go 输出目录
--go-grpc_outprotoc-gen-go-grpc 生成的 _grpc.pb.go 输出目录
--zrpc_outgo-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: 2000

3. 负载均衡与重试

go-zero 客户端默认使用 p2c 负载均衡算法。p2c(Power of Two Choices)会随机选两个实例,再选负载较低的那个,比简单轮询更智能,能避免把请求打到最繁忙的实例上。

p2c 的核心思想:

  1. 随机选两个后端实例
  2. 比较两者的负载(连接数、延迟、成功率等综合指标)
  3. 选择负载较低的那个

这种算法实现简单,但在大规模集群下表现优于轮询,能有效避免「热点实例」问题。

重试机制通过 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: 2000

ServiceContext 注入 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.rpc

logic/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: 2000

go.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-rpc

ServiceContext

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)。