Go-Zero RPC + API 网关示例

rpcandapi(Go-Zero RPC + API 网关示例)

模块名 : rpcandapi

Go 版本 : 1.25.10

框架 : go-zero v1.10.2

通信协议 : gRPC(Protobuf)+ HTTP RESTful

服务注册/发现: etcd


一、项目概述

1.1 项目定位

本项目是一个微服务架构的最小示例,演示在一个 Go 模块中同时构建:

  • RPC 服务端(gRPC):对外暴露 gRPC 接口,处理核心业务逻辑
  • API 网关(HTTP RESTful):对外暴露 HTTP 接口,内部通过 gRPC 客户端调用 RPC 服务

1.2 业务场景

提供 用户查询 功能:客户端通过 HTTP GET /user/:id 或直接通过 gRPC 调用 User.GetUser,获取指定 ID 的用户信息(ID + Name)。

1.3 架构拓扑

复制代码
外部客户端(HTTP)
     │
     ▼
┌─────────────┐    gRPC 调用    ┌──────────────┐
│  API 网关    │ ───────────────▶│  RPC 服务端   │
│  (Port 8888) │                 │  (Port 8080) │
└─────────────┘                 └──────┬───────┘
                                       │
                                       │ 服务注册
                                       ▼
                                ┌─────────────┐
                                │    etcd      │
                                │ (127.0.0.1:  │
                                │    2379)     │
                                └─────────────┘

二、目录结构

复制代码
rpcandapi/
├── DESIGN.md                          ← 项目设计文档(本文件)
├── go.mod                             ← Go 模块定义与依赖声明
├── go.sum                             ← 依赖版本锁定文件
├── user.go                            ← RPC 服务端主入口(main 函数)
├── user.proto                         ← Protobuf 协议定义(RPC 层)
├── user.api                           ← API 定义文件(HTTP 层,goctl 模板源)
├── cmd/
│   └── api/
│       └── main.go                    ← API 网关主入口(独立二进制)
├── etc/
│   ├── user.yaml                      ← RPC 服务端运行配置
│   └── user-api.yaml                  ← API 网关运行配置
├── user/                              ← Protobuf 生成的 Go 代码
│   ├── user.pb.go                     ←   Message 序列化/反序列化
│   └── user_grpc.pb.go                ←   gRPC Server/Client 桩代码
├── userclient/                        ← goctl 生成的 RPC 客户端封装
│   └── user.go                        ←   类型别名与客户端代理
└── internal/
    ├── config/
    │   └── config.go                  ← 配置结构体定义
    ├── svc/
    │   └── servicecontext.go          ← 服务上下文(依赖注入容器)
    ├── server/
    │   └── userserver.go              ← gRPC 服务实现入口
    ├── logic/
    │   ├── getuserlogic.go            ← RPC 服务端业务逻辑
    │   └── getuserapilogic.go         ← API 网关业务逻辑
    ├── handler/
    │   ├── routes.go                  ← HTTP 路由注册
    │   └── getuserhandler.go          ← HTTP 请求处理器
    └── types/
        └── types.go                   ← API 请求/响应类型定义

三、协议与接口定义

3.1 Protobuf 协议(user.proto

gRPC 层的通信协议,使用 Proto3 语法:

复制代码
syntax = "proto3";
package user;
option go_package="./user";

message GetUserRequest {
  int64 id = 1;          // 用户ID,字段编号1
}

message GetUserResponse {
  int64 id = 1;          // 用户ID
  string name = 2;       // 用户名称
}

service User {
  rpc GetUser(GetUserRequest) returns(GetUserResponse);
}

代码生成产物 (由 protoc + protoc-gen-go + protoc-gen-go-grpc 生成):

文件 内容 说明
user/user.pb.go GetUserRequest / GetUserResponse 结构体 消息序列化/反序列化,实现 proto.Message 接口
user/user_grpc.pb.go UserClient / UserServer 接口 + RegisterUserServer gRPC 客户端桩与服务端注册

gRPC 方法全限定名/user.User/GetUser

3.2 API 定义(user.api

go-zero 框架的 DSL,用于生成 HTTP 网关代码:

复制代码
type (
    GetUserReq {
        Id int64 `path:"id"`   // 从URL路径提取参数
    }
    GetUserResp {
        Id   int64  `json:"id"`
        Name string `json:"name"`
    }
)

service user-api {
    @handler getUser
    get /user/:id (GetUserReq) returns (GetUserResp)
}

代码生成产物 (由 goctl api go 生成):

文件 内容
internal/types/types.go 请求/响应 Go 结构体
internal/handler/routes.go HTTP 路由注册函数
internal/handler/getuserhandler.go HTTP Handler 脚手架

四、配置体系

4.1 配置结构体(internal/config/config.go

复制代码
// Config --- RPC 服务端配置,嵌入框架内置的 RpcServerConf
// 对应配置文件: etc/user.yaml(含 ListenOn、Etcd 等字段)
type Config struct {
    zrpc.RpcServerConf
}

// ApiConfig --- API 网关配置,嵌入 rest.RestConf 启动 HTTP 服务
// 对应配置文件: etc/user-api.yaml(含 Host、Port、UserRpc 等字段)
type ApiConfig struct {
    rest.RestConf                // HTTP 服务基础配置(Name, Host, Port)
    UserRpc  zrpc.RpcClientConf  // 调用下游 user.rpc 的客户端配置(服务发现+负载均衡)
}

4.2 RPC 服务端配置(etc/user.yaml

复制代码
Name: user.rpc              # 服务名称,etcd 中的 key 前缀
ListenOn: 0.0.0.0:8080      # gRPC 监听地址与端口
Etcd:                       # 服务注册中心
  Hosts:
  - 127.0.0.1:2379          # etcd 集群地址
  Key: user.rpc             # 服务注册 key

字段详细说明

字段 类型 说明
Name string 服务名,用于日志/监控标识,与 etcd Key 配合使用
ListenOn string 监听地址,格式 IP:Port0.0.0.0 表示监听所有网卡
Etcd.Hosts \[\]string etcd 集群节点列表,支持多个地址做高可用
Etcd.Key string 在 etcd 中注册的服务 key,其他服务通过此 key 发现该服务

4.3 API 网关配置(etc/user-api.yaml

复制代码
Name: user-api              # API 网关服务名称
Host: 0.0.0.0               # HTTP 监听地址
Port: 8888                  # HTTP 监听端口
UserRpc:                    # 下游 RPC 客户端配置
  Etcd:
    Hosts:
    - 127.0.0.1:2379        # etcd 地址(用于发现 user.rpc 服务)
    Key: user.rpc           # 目标服务的 etcd 注册 key

字段详细说明

字段 类型 说明
Name string API 网关服务名称
Host string HTTP 监听地址,0.0.0.0 监听所有网卡
Port int HTTP 监听端口
UserRpc.Etcd.Hosts \[\]string etcd 集群地址,用于发现下游 RPC 服务
UserRpc.Etcd.Key string 目标 RPC 服务在 etcd 中的注册 key

五、依赖注入体系(ServiceContext)

文件:internal/svc/servicecontext.go

5.1 RPC 服务端上下文

复制代码
type ServiceContext struct {
    Config config.Config    // RPC 服务端配置
}

func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{Config: c}
}
  • 职责:为 RPC 服务端的 logic 层提供配置和共享资源
  • 生命周期 :在 main() 中创建一次,贯穿整个进程生命周期
  • 扩展点 :后续可添加数据库连接 *sql.DB、Redis 客户端、其他 RPC 客户端等

5.2 API 网关上下文

复制代码
type ApiServiceContext struct {
    Config  config.ApiConfig   // API 网关配置
    UserRpc user.UserClient    // 下游 RPC 客户端(调用 user.rpc 服务)
}

func NewApiServiceContext(c config.ApiConfig) *ApiServiceContext {
    cli := zrpc.MustNewClient(c.UserRpc)     // 创建 zrpc 客户端(含服务发现)
    return &ApiServiceContext{
        Config:  c,
        UserRpc: user.NewUserClient(cli.Conn()),  // 包装为 gRPC 客户端
    }
}
  • UserRpc 字段 :持有到 user.rpc 服务的 gRPC 连接,内部通过 etcd 做服务发现和负载均衡
  • zrpc.MustNewClient:创建 go-zero 框架的 RPC 客户端,支持服务发现、超时、重试、熔断等能力
  • cli.Conn() :获取底层 grpc.ClientConnInterface,传给 Protobuf 生成的 NewUserClient

六、核心流程详解

6.1 RPC 服务端启动流程

复制代码
main() [user.go]
  │
  ├─1. flag.Parse()                         ← 解析命令行参数(-f 指定配置文件)
  │
  ├─2. conf.MustLoad("etc/user.yaml", &c)   ← 将 YAML 反序列化为 Config 结构体
  │
  ├─3. svc.NewServiceContext(c)             ← 创建服务上下文(依赖注入容器)
  │
  ├─4. zrpc.MustNewServer(c.RpcServerConf)  ← 创建 gRPC 服务器
  │     └─ 回调: user.RegisterUserServer()   ← 注册 UserServer 实现到 gRPC
  │     └─ reflection.Register()             ← (Dev/Test 模式) 注册 gRPC 反射
  │
  ├─5. defer s.Stop()                       ← 注册优雅退出
  │
  └─6. s.Start()                            ← 启动服务(阻塞,同时向 etcd 注册)

关键步骤详述

步骤 说明
conf.MustLoad 从 YAML 文件加载配置到结构体,失败时直接 panic。会自动填充 RpcServerConf 的默认值
zrpc.MustNewServer 创建 go-zero 封装的 gRPC 服务器,内置了服务注册、拦截器链、日志、监控等能力
RegisterUserServer 将由 protoc 生成的 gRPC 桩代码完成:将我们的 UserServer 实现绑定到 /user.User/GetUser 路由
reflection.Register 仅在开发/测试模式下开启 gRPC 反射,方便使用 grpcurl 等工具调试
s.Start() 阻塞启动,内部执行:① 启动 gRPC 端口监听 ② 向 etcd 注册服务 ③ 进入请求处理循环

6.2 RPC 请求处理链路

复制代码
gRPC 客户端请求
     │
     ▼
┌──────────────────────┐
│  gRPC Server (go-zero)│  接收请求,反序列化 protobuf
└────────┬─────────────┘
         │ 调用
         ▼
┌──────────────────────┐
│  server/UserServer    │  gRPC 桩 → 业务实现的分发层
│  .GetUser(ctx, in)    │  in = *user.GetUserRequest
└────────┬─────────────┘
         │ 委托
         ▼
┌──────────────────────┐
│  logic/GetUserLogic   │  纯业务逻辑层(无框架依赖)
│  .GetUser(in)         │  当前返回示例数据 "User Name"
└────────┬─────────────┘
         │ 返回
         ▼
   *user.GetUserResponse{Id: in.Id, Name: "User Name"}
         │
         ▼  gRPC 序列化 → 响应客户端

6.3 API 网关请求处理链路

复制代码
HTTP 客户端
     │  GET /user/123
     ▼
┌──────────────────────┐
│  rest.Server (go-zero)│  HTTP 服务器,路由匹配
└────────┬─────────────┘
         │ 匹配到 GET /user/:id
         ▼
┌──────────────────────┐
│  handler/routes.go    │  路由表注册
│  getUserHandler()     │
└────────┬─────────────┘
         │ 进入 Handler
         ▼
┌──────────────────────┐
│  httpx.Parse(r, &req) │  解析 HTTP 请求:
│                       │  - path 参数 /user/:id → req.Id
└────────┬─────────────┘
         │ req = &types.GetUserReq{Id: 123}
         ▼
┌──────────────────────┐
│  logic/GetUserApiLogic│  API 网关业务逻辑
│  .GetUser(&req)       │
└────────┬─────────────┘
         │ 构造 gRPC 请求
         │ user.GetUserRequest{Id: 123}
         ▼
┌──────────────────────┐
│  svc.UserRpc.GetUser()│  gRPC 客户端调用
│  (通过 etcd 服务发现)  │
└────────┬─────────────┘
         │ 网络调用 → RPC 服务端 :8080
         │ 收到响应: {Id: 123, Name: "User Name"}
         ▼
┌──────────────────────┐
│  httpx.OkJsonCtx()    │  返回 HTTP 200 + JSON
│  {"id":123,"name":"   │
│   User Name"}         │
└──────────────────────┘

七、各文件职责与说明

7.1 入口文件

7.1.1 RPC 服务端入口:user.go
属性 内容
文件路径 user.go(模块根目录)
所属包 package main
生成方式 手动编写 / goctl 脚手架
核心依赖 flaggo-zero/zrpcgRPC
默认配置文件 etc/user.yaml

关键代码段解读

复制代码
// 1. 命令行参数:通过 -f 指定配置文件路径,默认 etc/user.yaml
var configFile = flag.String("f", "etc/user.yaml", "the config file")

// 2. 加载配置(失败直接 panic),Config 内嵌 RpcServerConf
var c config.Config
conf.MustLoad(*configFile, &c)

// 3. 创建 gRPC Server,在回调中注册服务实现
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
    user.RegisterUserServer(grpcServer, server.NewUserServer(ctx))
    if c.Mode == service.DevMode || c.Mode == service.TestMode {
        reflection.Register(grpcServer)  // 开发/测试模式开启反射
    }
})

// 4. 启动(阻塞),同时向 etcd 注册服务
defer s.Stop()
s.Start()
7.1.2 API 网关入口:cmd/api/main.go
属性 内容
文件路径 cmd/api/main.go
所属包 package main(独立二进制)
生成方式 手动编写
核心依赖 flaggo-zero/restgRPC
默认配置文件 etc/user-api.yaml

说明 :与 user.go 分开放置在 cmd/api/ 子目录,避免同目录下两个 main() 冲突。

关键代码段解读

复制代码
// 1. 加载 API 网关配置(ApiConfig 内嵌 rest.RestConf + UserRpc)
var c config.ApiConfig
conf.MustLoad(*configFile, &c)

// 2. 创建 API 上下文(含下游 RPC 客户端)
ctx := svc.NewApiServiceContext(c)

// 3. 创建 HTTP 服务器,注册路由
server := rest.MustNewServer(c.RestConf)
handler.RegisterHandlers(server, ctx)

// 4. 启动 HTTP 服务
defer server.Stop()
server.Start()

7.2 gRPC 服务实现:internal/server/userserver.go

属性 内容
文件路径 internal/server/userserver.go
所属包 package server
生成方式 goctl 自动生成
实现接口 user.UserServer

嵌入 UnimplementedUserServer 的作用

  • gRPC 最佳实践要求服务端 struct 必须嵌入 UnimplementedUserServer

  • 当后续在 proto 中新增方法时,未更新的服务端仍能编译通过(新方法返回 Unimplemented 错误)

  • 测试阶段会检查嵌入方式(by value vs by pointer),防止空指针 panic

    type UserServer struct {
    svcCtx *svc.ServiceContext // 服务上下文,可访问配置和共享资源
    user.UnimplementedUserServer // 向前兼容的默认实现
    }

    func (s *UserServer) GetUser(ctx context.Context, in *user.GetUserRequest) (*user.GetUserResponse, error) {
    l := logic.NewGetUserLogic(ctx, s.svcCtx) // 创建业务逻辑实例(携带上下文)
    return l.GetUser(in) // 委托给 logic 层
    }

7.3 业务逻辑层

7.3.1 RPC 服务端逻辑:internal/logic/getuserlogic.go
属性 内容
文件路径 internal/logic/getuserlogic.go
所属包 package logic
接口签名 GetUser(*user.GetUserRequest) (*user.GetUserResponse, error)
复制代码
type GetUserLogic struct {
    ctx    context.Context          // 请求上下文(用于超时控制、trace 传递)
    svcCtx *svc.ServiceContext      // 服务上下文(访问配置、数据库等共享资源)
    logx.Logger                     // 嵌入日志记录器(自动注入 traceId)
}

func (l *GetUserLogic) GetUser(in *user.GetUserRequest) (*user.GetUserResponse, error) {
    // TODO: 此处应实现实际的数据库查询逻辑
    return &user.GetUserResponse{
        Id:   in.Id,
        Name: "User Name",          // 示例硬编码数据
    }, nil
}

设计要点

  • logx.Logger 为嵌入字段:可直接 l.Infof(...) 打印日志,自动带上 traceId
  • ctx 透传:支持超时取消、分布式追踪(OpenTelemetry)
  • 返回值始终为 protobuf 类型:接口契约清晰
7.3.2 API 网关逻辑:internal/logic/getuserapilogic.go
属性 内容
文件路径 internal/logic/getuserapilogic.go
所属包 package logic
接口签名 GetUser(*types.GetUserReq) (*types.GetUserResp, error)
复制代码
type GetUserApiLogic struct {
    ctx    context.Context
    svcCtx *svc.ApiServiceContext   // API 网关上下文(含 RPC 客户端)
    logx.Logger
}

func (l *GetUserApiLogic) GetUser(req *types.GetUserReq) (*types.GetUserResp, error) {
    // 1. 将 API 类型转换为 proto 类型
    // 2. 通过 RPC 客户端调用下游 gRPC 服务
    resp, err := l.svcCtx.UserRpc.GetUser(l.ctx, &user.GetUserRequest{Id: req.Id})
    if err != nil {
        return nil, err
    }
    // 3. 将 proto 响应转换为 API 响应类型
    return &types.GetUserResp{Id: resp.Id, Name: resp.Name}, nil
}

设计要点

  • 类型转换 :在 API 类型(types.GetUserReq)和 RPC 类型(user.GetUserRequest)之间做转换,保持各层类型独立
  • 错误透传 :gRPC 错误直接返回给 HTTP 层,go-zero 的 httpx.ErrorCtx 会自动设置恰当的 HTTP 状态码
  • 超时传播l.ctx 从 HTTP 请求传递到 gRPC 调用,形成完整的超时链路

7.4 HTTP 路由注册:internal/handler/routes.go

复制代码
func RegisterHandlers(server *rest.Server, serverCtx *svc.ApiServiceContext) {
    server.AddRoutes(
        []rest.Route{
            {
                Method:  http.MethodGet,         // HTTP GET 方法
                Path:    "/user/:id",             // URL 路径,:id 为路径参数
                Handler: getUserHandler(serverCtx), // 请求处理器
            },
        },
    )
}

7.5 HTTP 请求处理器:internal/handler/getuserhandler.go

复制代码
func getUserHandler(svcCtx *svc.ApiServiceContext) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        var req types.GetUserReq
        if err := httpx.Parse(r, &req); err != nil {    // 自动解析 path/query/body 参数
            httpx.ErrorCtx(r.Context(), w, err)          // 参数错误 → 400
            return
        }
        l := logic.NewGetUserApiLogic(r.Context(), svcCtx)
        resp, err := l.GetUser(&req)
        if err != nil {
            httpx.ErrorCtx(r.Context(), w, err)          // 业务错误 → 根据 gRPC status 映射 HTTP 状态码
        } else {
            httpx.OkJsonCtx(r.Context(), w, resp)        // 成功 → 200 + JSON
        }
    }
}

7.6 RPC 客户端封装:userclient/user.go

属性 内容
文件路径 userclient/user.go
所属包 package userclient
生成方式 goctl 自动生成
复制代码
// 类型别名:方便外部引用
type GetUserRequest  = user.GetUserRequest
type GetUserResponse = user.GetUserResponse

// 接口定义
type User interface {
    GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
}

// 默认实现(内部使用 zrpc.Client 做服务发现)
type defaultUser struct {
    cli zrpc.Client
}

func (m *defaultUser) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) {
    client := user.NewUserClient(m.cli.Conn())       // 获取原生 gRPC 客户端
    return client.GetUser(ctx, in, opts...)
}

说明 :项目中 API 网关直接使用了 user.NewUserClient(cli.Conn()),而非该封装。两种方式等效。


八、技术栈与依赖说明

8.1 核心依赖

依赖 版本 用途
github.com/zeromicro/go-zero v1.10.2 微服务框架核心(配置加载、RPC/HTTP 服务器、服务注册发现)
google.golang.org/grpc v1.81.1 gRPC 通信库
google.golang.org/protobuf v1.36.11 Protobuf 序列化/反序列化

8.2 基础设施依赖(间接)

组件 版本 用途
go.etcd.io/etcd/client/v3 v3.5.21 etcd 客户端,服务注册与发现
github.com/prometheus/client_golang v1.23.2 Prometheus 指标采集
github.com/redis/go-redis/v9 v9.19.0 Redis 客户端(框架内置,当前未使用)
go.opentelemetry.io/otel v1.43.0 OpenTelemetry 分布式链路追踪
github.com/openzipkin/zipkin-go v0.4.3 Zipkin 链路追踪上报

8.3 代码生成工具

工具 用途 对应产物
protoc + protoc-gen-go + protoc-gen-go-grpc .proto 生成 gRPC 代码 user/user.pb.gouser/user_grpc.pb.go
goctl v1.10.1 .api 生成 HTTP 网关代码;从 .proto 生成客户端封装 internal/handler/internal/types/userclient/

九、运行指南

9.1 环境前提

组件 要求
Go ≥ 1.25.10
etcd 运行在 127.0.0.1:2379
防火墙 允许 8080 (gRPC) 和 8888 (HTTP) 端口

9.2 编译

复制代码
# 编译 RPC 服务端
go build -o user.exe user.go

# 编译 API 网关
go build -o api.exe cmd/api/main.go

# 全量编译检查(注意:根目录和 cmd/api/ 各有一个 main,go build ./... 会检查所有子包)
go build ./...

9.3 启动服务(完整步骤)

第一步:启动 etcd

关键前提:etcd 是服务注册与发现的中心节点。RPC 服务端启动时会向 etcd 注册自己,API 网关启动时会从 etcd 发现下游 RPC 服务地址。如果 etcd 未运行,两个服务都无法正常启动。

复制代码
# 直接启动 etcd(需提前安装)
etcd

# 预期日志输出:
# {"level":"info","ts":"...","caller":"etcdmain/etcd.go:...","msg":"....","name":"default","data dir":"default.etcd",...}
# {"level":"info","ts":"...","caller":"embed/etcd.go:...","msg":"serving client requests on 127.0.0.1:2379"}

验证 etcd 已就绪:

复制代码
etcdctl endpoint health
# 输出: 127.0.0.1:2379 is healthy: successfully committed proposal: ...
第二步:启动 RPC 服务端

注意 :RPC 服务端必须使用 etc/user.yaml,不能用 etc/user-api.yaml

复制代码
# 打开终端1,在项目根目录执行:
go run user.go -f etc/user.yaml

预期输出:

复制代码
Starting rpc server at 0.0.0.0:8080...

验证方式:

复制代码
# 如果配置了 DevMode/TestMode,可用 grpcurl 直接测试
grpcurl -plaintext -d '{"id": 123}' 127.0.0.1:8080 user.User/GetUser
# 返回: { "id": "123", "name": "User Name" }
第三步:启动 API 网关

注意:必须等 RPC 服务端成功启动并注册到 etcd 后,再启动 API 网关。否则网关无法发现下游服务。

复制代码
# 打开终端2(保持终端1继续运行),在项目根目录执行:
go run cmd/api/main.go -f etc/user-api.yaml

预期输出:

复制代码
Starting api server at 0.0.0.0:8888...

验证方式:

复制代码
curl http://127.0.0.1:8888/user/123
# 返回: {"id":123,"name":"User Name"}
完整启动流程图
复制代码
┌──────────────────────────────────────────────────────┐
│  步骤1: 启动 etcd (127.0.0.1:2379)                    │
│  │                                                    │
│  ▼                                                    │
│  步骤2: 启动 RPC 服务端 (go run user.go -f etc/...)   │
│  │         → 监听 :8080 gRPC 端口                     │
│  │         → 向 etcd 注册服务 key: user.rpc            │
│  ▼                                                    │
│  步骤3: 启动 API 网关 (go run cmd/api/main.go -f ...) │
│           → 从 etcd 发现 user.rpc 服务                 │
│           → 监听 :8888 HTTP 端口                       │
│  ▼                                                    │
│  验证: curl http://127.0.0.1:8888/user/123             │
└──────────────────────────────────────────────────────┘
常见启动错误及解决
错误信息 原因 解决
field "ListenOn" is not set RPC 入口 user.go 误用了 etc/user-api.yaml 改用 -f etc/user.yaml
field "Host" is not set API 入口 cmd/api/main.go 误用了 etc/user.yaml 改用 -f etc/user-api.yaml
context deadline exceeded etcd 未启动或不可达 先启动 etcd,确认 127.0.0.1:2379 可访问
rpc error: code = Unavailable API 网关启动时 RPC 服务端未就绪 确保 RPC 服务端已先启动并注册到 etcd
bind: address already in use 端口已被占用 检查 8080(gRPC) 和 8888(HTTP) 是否被其他进程占用

9.4 测试调用

复制代码
# === 方式一:通过 gRPC 直接调用(需安装 grpcurl)===
grpcurl -plaintext -d '{"id": 123}' 127.0.0.1:8080 user.User/GetUser
# 返回: { "id": "123", "name": "User Name" }

# === 方式二:通过 API 网关 HTTP 调用 ===
curl http://127.0.0.1:8888/user/123
# 返回: {"id":123,"name":"User Name"}

十、分层架构与设计理念

10.1 分层图

复制代码
┌──────────────────────────────────────────┐
│           Entry (main)                   │  ← 入口层:解析参数、加载配置、组装启动
├──────────────────────────────────────────┤
│  Handler  │  Server                      │  ← 接入层:HTTP Handler / gRPC Server 桩
├──────────────────────────────────────────┤
│               Logic                      │  ← 业务逻辑层:纯业务代码,无框架协议依赖
├──────────────────────────────────────────┤
│             ServiceContext (SVC)          │  ← 依赖注入层:配置、DB、RPC 客户端等共享资源
├──────────────────────────────────────────┤
│               Config                     │  ← 配置层:YAML → Struct 映射
└──────────────────────────────────────────┘

10.2 核心设计理念

1. 类型分层独立

使用的类型 来源
gRPC Server user.GetUserRequest / user.GetUserResponse protobuf 生成
API Handler types.GetUserReq / types.GetUserResp goctl 生成
RPC Logic 操作 protobuf 类型,调用 DB/缓存等基础设施 ---
API Logic 将 API 类型转为 protobuf 类型,调用 RPC 客户端 ---

2. 最小化依赖原则

  • Logic 层不需要直接导入 gRPCnet/http
  • Handler 层不需要知道 protobuf 的存在
  • 各层通过 ServiceContext 获取依赖

3. 错误处理链路

复制代码
Logic 返回 error
      │
      ▼
Server/Handler 不处理 error,直接返回
      │
      ▼
go-zero 框架自动映射:
  - gRPC: error → gRPC status code
  - HTTP: gRPC status code → HTTP status code + JSON error body

十一、扩展指南

11.1 添加新接口

以添加 "创建用户" 方法为例:

步骤1:扩展 user.proto

复制代码
message CreateUserRequest { string name = 1; }
message CreateUserResponse { int64 id = 1; string name = 2; }

service User {
    rpc GetUser(GetUserRequest) returns(GetUserResponse);
    rpc CreateUser(CreateUserRequest) returns(CreateUserResponse);  // 新增
}

步骤2:重新生成 gRPC 代码

复制代码
protoc --go_out=. --go-grpc_out=. user.proto

步骤3:添加业务逻辑internal/logic/createuserlogic.go

复制代码
func (l *CreateUserLogic) CreateUser(in *user.CreateUserRequest) (*user.CreateUserResponse, error) {
    // DB 插入逻辑
    return &user.CreateUserResponse{Id: 1, Name: in.Name}, nil
}

步骤4:在 UserServer 中实现新方法

复制代码
func (s *UserServer) CreateUser(ctx context.Context, in *user.CreateUserRequest) (*user.CreateUserResponse, error) {
    l := logic.NewCreateUserLogic(ctx, s.svcCtx)
    return l.CreateUser(in)
}

步骤5:扩展 user.api 并重新生成 HTTP 网关代码

复制代码
goctl api go -api user.api -dir .

11.2 连接数据库

ServiceContext 中添加 DB 连接:

复制代码
type ServiceContext struct {
    Config config.Config
    DB     *gorm.DB       // 新增
}

func NewServiceContext(c config.Config) *ServiceContext {
    db, _ := gorm.Open(mysql.Open(c.DB.DSN), &gorm.Config{})
    return &ServiceContext{Config: c, DB: db}
}

11.3 添加缓存

复制代码
type ServiceContext struct {
    Config config.Config
    Redis  *redis.Client  // 新增
}

11.4 API 网关独立入口(已实现)

项目已包含 API 网关入口 cmd/api/main.go,放在子目录避免与 user.gomain() 冲突。

复制代码
// cmd/api/main.go
package main

import (
    "flag"
    "fmt"
    "rpcandapi/internal/config"
    "rpcandapi/internal/handler"
    "rpcandapi/internal/svc"
    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/rest"
)

var configFile = flag.String("f", "etc/user-api.yaml", "the api config file")

func main() {
    flag.Parse()
    var c config.ApiConfig
    conf.MustLoad(*configFile, &c)
    ctx := svc.NewApiServiceContext(c)
    server := rest.MustNewServer(c.RestConf)
    defer server.Stop()
    handler.RegisterHandlers(server, ctx)
    fmt.Printf("Starting api server at %s:%d...\n", c.Host, c.Port)
    server.Start()
}

运行方式:go run cmd/api/main.go -f etc/user-api.yaml


十二、注意事项

启动相关

  1. 配置文件不可混用user.go(RPC 服务端)必须用 etc/user.yamlcmd/api/main.go(API 网关)必须用 etc/user-api.yaml。混用会导致 field "ListenOn" is not set 等运行时错误
  2. etcd 必需性:RPC 服务端启动时会向 etcd 注册,若 etcd 不可用(未启动或网络不通),服务启动会失败并退出
  3. 启动顺序不可逆:必须先启动 etcd → 再启动 RPC 服务端 → 最后启动 API 网关。API 网关启动时会立刻从 etcd 发现下游服务,如果 RPC 服务端尚未注册,网关将无法正常处理请求
  4. 两个 main 入口 :项目有两个可执行入口------根目录 user.go(RPC 服务端)和 cmd/api/main.go(API 网关),分别编译为两个独立二进制,需要在两个不同的终端窗口中运行
  5. 端口占用 :运行前确保 8080(gRPC)和 8888(HTTP)端口未被其他进程占用。可通过 netstat -ano | findstr "8080"netstat -ano | findstr "8888" 检查

开发与部署

  1. Protobuf 兼容性 :生成代码使用 proto3 语法,消息中无 required/optional 标记,所有字段默认可选。修改 .proto 后必须重新运行 protoc 生成代码
  2. gRPC 反射 :仅 Dev/Test 模式开启,生产环境应关闭以避免敏感接口信息泄露。配置文件中可通过 Mode 字段控制
  3. 配置文件路径 :入口文件硬编码默认路径为 etc/ 目录下对应文件。部署时如配置文件路径变更,需通过 -f 参数显式指定;如 -f 不传则使用代码中的默认路径
  4. 跨平台编译 :项目使用 go 1.25.10,注意目标环境的 Go 版本兼容性。不同操作系统下二进制后缀名不同(Windows 为 .exe,Linux/macOS 无后缀)
  5. 优雅退出user.gocmd/api/main.go 均通过 defer s.Stop() / defer server.Stop() 实现优雅退出。收到 SIGINT(Ctrl+C)或 SIGTERM 时,框架会自动处理:
    • RPC 服务端:从 etcd 注销 → 拒绝新请求 → 处理完在途请求 → 关闭端口
    • API 网关:拒绝新连接 → 处理完在途请求 → 关闭端口

测试调试

  1. 测试工具 :推荐使用 grpcurl 调试 gRPC 接口,curl 调试 HTTP 接口。前提是配置文件中的 Mode 设置为 devtest,以开启 gRPC 反射
  2. 网络可达性 :所有服务默认监听 0.0.0.0(所有网卡),测试时请使用 127.0.0.1。若客户端与服务端不在同一台机器,请确保防火墙放行对应端口