rpcandapi(Go-Zero RPC + API 网关示例)
模块名 :
rpcandapiGo 版本 : 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:Port,0.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 脚手架 |
| 核心依赖 | flag、go-zero/zrpc、gRPC |
| 默认配置文件 | 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(独立二进制) |
| 生成方式 | 手动编写 |
| 核心依赖 | flag、go-zero/rest、gRPC |
| 默认配置文件 | 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(...)打印日志,自动带上 traceIdctx透传:支持超时取消、分布式追踪(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.go、user/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 层不需要直接导入
gRPC或net/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.go 的 main() 冲突。
// 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
十二、注意事项
启动相关
- 配置文件不可混用 :
user.go(RPC 服务端)必须用etc/user.yaml,cmd/api/main.go(API 网关)必须用etc/user-api.yaml。混用会导致field "ListenOn" is not set等运行时错误 - etcd 必需性:RPC 服务端启动时会向 etcd 注册,若 etcd 不可用(未启动或网络不通),服务启动会失败并退出
- 启动顺序不可逆:必须先启动 etcd → 再启动 RPC 服务端 → 最后启动 API 网关。API 网关启动时会立刻从 etcd 发现下游服务,如果 RPC 服务端尚未注册,网关将无法正常处理请求
- 两个 main 入口 :项目有两个可执行入口------根目录
user.go(RPC 服务端)和cmd/api/main.go(API 网关),分别编译为两个独立二进制,需要在两个不同的终端窗口中运行 - 端口占用 :运行前确保
8080(gRPC)和8888(HTTP)端口未被其他进程占用。可通过netstat -ano | findstr "8080"和netstat -ano | findstr "8888"检查
开发与部署
- Protobuf 兼容性 :生成代码使用 proto3 语法,消息中无
required/optional标记,所有字段默认可选。修改.proto后必须重新运行protoc生成代码 - gRPC 反射 :仅 Dev/Test 模式开启,生产环境应关闭以避免敏感接口信息泄露。配置文件中可通过
Mode字段控制 - 配置文件路径 :入口文件硬编码默认路径为
etc/目录下对应文件。部署时如配置文件路径变更,需通过-f参数显式指定;如-f不传则使用代码中的默认路径 - 跨平台编译 :项目使用
go 1.25.10,注意目标环境的 Go 版本兼容性。不同操作系统下二进制后缀名不同(Windows 为.exe,Linux/macOS 无后缀) - 优雅退出 :
user.go和cmd/api/main.go均通过defer s.Stop()/defer server.Stop()实现优雅退出。收到 SIGINT(Ctrl+C)或 SIGTERM 时,框架会自动处理:- RPC 服务端:从 etcd 注销 → 拒绝新请求 → 处理完在途请求 → 关闭端口
- API 网关:拒绝新连接 → 处理完在途请求 → 关闭端口
测试调试
- 测试工具 :推荐使用
grpcurl调试 gRPC 接口,curl调试 HTTP 接口。前提是配置文件中的Mode设置为dev或test,以开启 gRPC 反射 - 网络可达性 :所有服务默认监听
0.0.0.0(所有网卡),测试时请使用127.0.0.1。若客户端与服务端不在同一台机器,请确保防火墙放行对应端口