ProtoBuf 与 gRPC

1948 字
10 分钟
ProtoBuf 与 gRPC

本文介绍如何使用 ProtoBuf 定义数据结构和服务接口,并在 Go 语言中实现 gRPC 服务端逻辑。请事先配置好 protoc 和 Go 的 gRPC 插件环境。

为什么我们需要 ProtoBuf?#

在微服务和网络通信中,我们需要一种方式来“打包”数据。你可能熟悉 JSON,它易读但体积大、解析慢。

Protocol Buffers (ProtoBuf) 是 Google 开发的一种机制。你可以把它想象成一种强类型的、经过极致压缩的 JSON。

  • 强类型:必须定义字段是 int 还是 string。
  • 契约优先:先写 .proto 文件定义数据结构和接口,再生成代码。

2. 编写你的第一个 ProtoBuf 文件#

ProtoBuf 使用 .proto 文件来描述数据。我们以一个“用户服务”为例。

2.1 头部声明#

每个 .proto 文件的开头都应该包含以下关键信息:

// 声明使用 proto3 语法(目前的主流版本,必写)
syntax = "proto3";
// 定义包名,类似于 C++ 的 namespace 或 Java 的 package
// 这防止了不同项目之间的命名冲突
package user.v1;
// 【关键】指定 Go 语言生成代码的路径
// 格式通常是: "模块名/路径;包名"
// - go_package 定义了生成的 .pb.go 文件属于哪个 Go包
option go_package = "[example.com/project/api/user;userpb](https://example.com/project/api/user;userpb)";

2.2 定义消息 (Message)#

message 类似于 Go 中的 struct。它是数据传输的基本单元。

// 定义一个用户实体
message User {
// 字段格式:类型 字段名 = 唯一编号;
// 唯一编号(Tag):1, 2, 3...
// 注意:这个数字非常重要,它是二进制编码时的字段标识,一旦定下来不要轻易修改!
int64 id = 1; // 用户ID
string username = 2; // 用户名
bool is_active = 3; // 是否激活
float balance = 4; // 余额
bytes avatar = 5; // 头像(二进制数据)
}

注意:ProtoBuf 中字段名推荐使用 下划线命名法 (snake_case),例如 is_active。生成的 Go 代码会自动转为 大驼峰 (PascalCase),即 IsActive。

2.3 复杂类型:数组与映射#

在实际业务中,我们经常需要列表或字典结构。

message UserProfile {
int64 user_id = 1;
// repeated 关键字表示数组(切片/List)
// 对应 Go 中的 []string
repeated string hobbies = 2;
// map 关键字表示映射
// 对应 Go 中的 map[string]string
map<string, string> metadata = 3;
}

2.4 枚举 (Enum)#

当字段只有固定的几个值时,使用枚举。

// 枚举通常建议包含一个 0 值作为默认值
enum UserStatus {
STATUS_UNKNOWN = 0; // 默认值,必须是0
STATUS_NORMAL = 1;
STATUS_BANNED = 2;
}
message UserStatusRequest {
int64 user_id = 1;
UserStatus status = 2; // 使用上面定义的枚举
}

3. 定义服务 (Service) —— gRPC 的核心#

仅仅定义数据结构(Message)是不够的,我们需要定义方法。在 ProtoBuf 中,这叫做 service。

这相当于定义 Go 中的 interface。

// 定义 User 服务
service UserService {
// 定义一个 RPC 方法:GetUser
// 接收 GetUserRequest,返回 GetUserResponse
rpc GetUser (GetUserRequest) returns (GetUserResponse);
// 定义另一个方法:CreateUser
rpc CreateUser (CreateUserRequest) returns (CreateUserResponse);
}
// ------------------------------------
// 配套的 Request 和 Response 消息定义
// ------------------------------------
message GetUserRequest {
int64 id = 1;
}
message GetUserResponse {
User user = 1; // 引用上面定义的 User 消息
}
message CreateUserRequest {
string username = 1;
string password = 2;
}
message CreateUserResponse {
int64 id = 1;
bool success = 2;
}

4. 从 Proto 到 Go 的映射原理#

假设你已经运行了生成命令(protoc),生成了 user.pb.go (数据结构) 和 user_grpc.pb.go (服务接口)。

你需要理解生成的代码是什么样子的,才能知道怎么去写业务逻辑。

4.1 Message Struct#

ProtoBuf 定义的 message 会被转化为 Go 的 struct。

Proto:

message User {
int64 id = 1;
string username = 2;
}

生成的 Go 代码 (近似):

type User struct {
Id int64 `protobuf:"varint,1,..." json:"id,omitempty"`
Username string `protobuf:"bytes,2,..." json:"username,omitempty"`
// 还有一些内部使用的字段...
}
  • 你可以直接像操作普通结构体一样操作它:user.Id = 100。

4.2 Service Interface#

ProtoBuf 定义的 service 会生成两个关键部分:客户端接口(Client)和 服务端接口(Server)。我们主要关注服务端。

Proto:

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

生成的 Go 接口 (在 _grpc.pb.go 中),统一在名为 _ServiceServer 的接口中,直接在此文件中查找:

// 这是你需要实现的接口!
type UserServiceServer interface {
// 上下文 Context 永远是第一个参数,用于处理超时和取消
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
// 这是一个为了向前兼容的安全方法,必须嵌入
mustEmbedUnimplementedUserServiceServer()
}

5. 核心实战:如何在 Go 中实现接口#

步骤 1: 创建结构体#

你需要定义一个结构体,作为服务的承载者。

package main
import (
"context"
"errors"
// 引入生成的 proto 包,假设别名为 pb
pb "[example.com/project/api/user](https://example.com/project/api/user)"
)
// 1. 定义结构体
type server struct {
// 2. 【必须】嵌入“未实现的服务端”结构体
// 这样做的好处是,如果你在 .proto 增加了新方法但还没写 Go 代码,
// 程序依然能编译通过(只不过调用新方法会报错“未实现”)。
pb.UnimplementedUserServiceServer
}

步骤 2: 实现方法#

完全照搬生成的接口签名来写方法,直接从 type _Server interface 粘贴复制来,并进行相应补全。

例如将 GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) 补全为 func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) 并实现业务逻辑。

// 实现 GetUser 方法
// 参数:ctx (上下文), req (请求体指针)
// 返回:resp (响应体指针), err (错误信息)
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 1. 获取请求参数
userID := req.GetId() // 建议使用 GetId() 方法而不是 .Id,防空指针
// 2. 执行业务逻辑 (模拟查库)
if userID == 0 {
return nil, errors.New("用户ID不能为空")
}
// 模拟数据
foundUser := &pb.User{
Id: userID,
Username: "DeepLearningUser",
IsActive: true,
Hobbies: []string{"coding", "reading"},
}
// 3. 构造响应并返回
return &pb.GetUserResponse{
User: foundUser,
}, nil
}

步骤 3: 启动 gRPC Server#

最后,你需要编写 main 函数来监听端口并注册你的服务。

package main
import (
"log"
"net"
"google.golang.org/grpc"
pb "[example.com/project/api/user](https://example.com/project/api/user)"
)
func main() {
// 1. 监听 TCP 端口
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("无法监听端口: %v", err)
}
// 2. 创建 gRPC 服务器实例
grpcServer := grpc.NewServer()
// 3. 注册服务
// 将我们实现的 server{} 结构体注册到 gRPC 服务器上
// 这个 Register 函数是自动生成的代码提供的
pb.RegisterUserServiceServer(grpcServer, &server{})
log.Printf("gRPC 服务启动在 :50051 端口...")
// 4. 开始服务 (阻塞操作)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("服务启动失败: %v", err)
}
}

6. 一个 .bat 脚本快速生成代码#

这个脚本可以帮助你快速生成 .pb.go _grpc.pb.go 和 _validate.pb.go 文件。

Terminal window
@echo off
setlocal
if "%~1"=="" (
echo [USAGE] Please provide a proto file.
exit /b 1
)
set "TARGET_PATH=%~dp1"
set "FILE_NAME=%~nx1"
pushd "%TARGET_PATH%"
echo [INFO] Working Directory: %CD%
protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --validate_out="lang=go:." "%FILE_NAME%"
if %errorlevel% neq 0 (
echo [FAIL] Compilation failed!
popd
exit /b 1
)
echo [SUCCESS] Done.
popd

cd 到你的 proto 文件目录,运行对应的 .proto 文件:

Terminal window
makeproto user.proto

7. 提示#

  1. 零值处理:ProtoBuf (proto3) 不会在网络中传输默认值(如 0, false, "")。
  • 如果你在 Go 中收到 0,你无法区分它是“未设置”还是“故意设置为 0”。
  • 如果必须区分,需要使用 Wrapper 类型(如 google.protobuf.Int32Value)或指针。
  1. 字段编号:一旦上线,不要更改字段的编号(Tag)。如果你要把 id = 1 改成 id = 2,旧版本的客户端将无法正确解析数据。
  2. Getter 方法:在 Go 中读取 proto 结构体字段时,尽量使用生成的 GetFieldName() 方法(例如 req.GetUsername())。如果 req 本身是 nil,直接访问 req.Username 会 Panic,而 req.GetUsername() 会安全返回空字符串。

8. 总结#

  1. 写契约:编写 xxx.proto 文件,定义 Message 和 Service。
  2. 生成代码:运行 protoc 生成 Go 代码。
  3. 定义 Struct:在 Go 中定义 type server struct {} 并嵌入 Unimplemented...。
  4. 实现逻辑:为 server 实现接口定义的方法 (ctx, req) -> (resp, err)。
  5. 启动注册:在 main 中 net.Listen -> grpc.NewServer -> Register... -> Serve。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

ProtoBuf 与 gRPC
https://www.lansganbs.cn/posts/项目开发/protobuf-与-grpc/
作者
Zowely
发布于
2025-03-28
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Zowely
红叶最多情,一舞寄相思。
公告
欢迎来到我的博客!这里分享计算机等相关内容。