3.7.2 Protocol Buffers #
Protocol Buffers(简称 protobuf)是 Google 开发的一种语言无关、平台无关的可扩展序列化结构数据的方法。它既可以用作通信协议,也可以用作数据存储格式。相比 JSON 和 XML,protobuf 更小、更快、更简单。
Protocol Buffers 基础 #
语法版本 #
Protocol Buffers 有两个主要版本:proto2 和 proto3。目前推荐使用 proto3,它语法更简洁,功能更强大。
// 指定语法版本(必须是文件的第一行非注释内容)
syntax = "proto3";
基本语法结构 #
syntax = "proto3";
// 包声明
package example.v1;
// Go 包路径选项
option go_package = "github.com/example/proto/example/v1";
// 导入其他 proto 文件
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
// 服务定义
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}
// 消息定义
message User {
int64 id = 1;
string name = 2;
string email = 3;
google.protobuf.Timestamp created_at = 4;
}
数据类型 #
标量类型 #
Protocol Buffers 支持多种标量类型:
message ScalarTypes {
// 数值类型
double double_value = 1; // 64位浮点数
float float_value = 2; // 32位浮点数
int32 int32_value = 3; // 32位整数
int64 int64_value = 4; // 64位整数
uint32 uint32_value = 5; // 32位无符号整数
uint64 uint64_value = 6; // 64位无符号整数
sint32 sint32_value = 7; // 32位有符号整数(更高效编码负数)
sint64 sint64_value = 8; // 64位有符号整数(更高效编码负数)
fixed32 fixed32_value = 9; // 32位固定长度
fixed64 fixed64_value = 10; // 64位固定长度
sfixed32 sfixed32_value = 11; // 32位固定长度有符号
sfixed64 sfixed64_value = 12; // 64位固定长度有符号
// 布尔类型
bool bool_value = 13;
// 字符串和字节
string string_value = 14; // UTF-8 编码字符串
bytes bytes_value = 15; // 任意字节序列
}
Go 类型映射 #
protobuf 类型 | Go 类型 | 说明 |
---|---|---|
double | float64 | 64 位浮点数 |
float | float32 | 32 位浮点数 |
int32 | int32 | 32 位整数 |
int64 | int64 | 64 位整数 |
uint32 | uint32 | 32 位无符号整数 |
uint64 | uint64 | 64 位无符号整数 |
sint32 | int32 | 有符号 32 位整数 |
sint64 | int64 | 有符号 64 位整数 |
fixed32 | uint32 | 固定 32 位 |
fixed64 | uint64 | 固定 64 位 |
sfixed32 | int32 | 有符号固定 32 位 |
sfixed64 | int64 | 有符号固定 64 位 |
bool | bool | 布尔值 |
string | string | UTF-8 字符串 |
bytes | []byte | 字节切片 |
复合类型 #
枚举类型 #
// 定义枚举
enum Status {
// 第一个枚举值必须是 0
STATUS_UNSPECIFIED = 0;
STATUS_PENDING = 1;
STATUS_APPROVED = 2;
STATUS_REJECTED = 3;
}
// 使用枚举
message Order {
int64 id = 1;
Status status = 2;
string description = 3;
}
在 Go 中的使用:
order := &pb.Order{
Id: 123,
Status: pb.Status_STATUS_PENDING,
Description: "Test order",
}
// 检查枚举值
if order.Status == pb.Status_STATUS_APPROVED {
fmt.Println("Order is approved")
}
嵌套消息 #
message User {
int64 id = 1;
string name = 2;
// 嵌套消息
message Address {
string street = 1;
string city = 2;
string country = 3;
string postal_code = 4;
}
Address address = 3;
}
// 也可以在外部定义
message Company {
string name = 1;
User.Address headquarters = 2; // 引用嵌套类型
}
重复字段(数组) #
message UserList {
// 重复字段相当于数组
repeated User users = 1;
repeated string tags = 2;
repeated int32 scores = 3;
}
在 Go 中:
userList := &pb.UserList{
Users: []*pb.User{
{Id: 1, Name: "Alice"},
{Id: 2, Name: "Bob"},
},
Tags: []string{"admin", "user"},
Scores: []int32{95, 87, 92},
}
Map 类型 #
message UserProfile {
int64 user_id = 1;
// Map 类型
map<string, string> metadata = 2; // 字符串到字符串的映射
map<int32, User> user_map = 3; // 整数到用户的映射
map<string, int32> counters = 4; // 字符串到计数器的映射
}
在 Go 中:
profile := &pb.UserProfile{
UserId: 123,
Metadata: map[string]string{
"department": "engineering",
"level": "senior",
},
UserMap: map[int32]*pb.User{
1: {Id: 1, Name: "Alice"},
2: {Id: 2, Name: "Bob"},
},
Counters: map[string]int32{
"login_count": 42,
"message_count": 156,
},
}
高级特性 #
Oneof 字段 #
Oneof 字段表示多个字段中只能设置一个:
message SearchRequest {
string query = 1;
oneof search_type {
string keyword = 2;
int32 user_id = 3;
string email = 4;
}
}
在 Go 中使用:
// 设置 keyword
req1 := &pb.SearchRequest{
Query: "test",
SearchType: &pb.SearchRequest_Keyword{
Keyword: "golang",
},
}
// 设置 user_id
req2 := &pb.SearchRequest{
Query: "test",
SearchType: &pb.SearchRequest_UserId{
UserId: 123,
},
}
// 检查设置的字段
switch x := req1.SearchType.(type) {
case *pb.SearchRequest_Keyword:
fmt.Printf("Searching by keyword: %s\n", x.Keyword)
case *pb.SearchRequest_UserId:
fmt.Printf("Searching by user ID: %d\n", x.UserId)
case *pb.SearchRequest_Email:
fmt.Printf("Searching by email: %s\n", x.Email)
default:
fmt.Println("No search type specified")
}
Any 类型 #
Any 类型可以包含任意的 protobuf 消息:
import "google/protobuf/any.proto";
message LogEntry {
string level = 1;
string message = 2;
google.protobuf.Any details = 3; // 可以包含任意消息
}
在 Go 中使用:
import (
"google.golang.org/protobuf/types/known/anypb"
)
// 创建 Any 类型
user := &pb.User{Id: 123, Name: "Alice"}
anyUser, err := anypb.New(user)
if err != nil {
log.Fatal(err)
}
logEntry := &pb.LogEntry{
Level: "INFO",
Message: "User created",
Details: anyUser,
}
// 解析 Any 类型
if logEntry.Details != nil {
var user pb.User
if err := logEntry.Details.UnmarshalTo(&user); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %+v\n", user)
}
字段选项 #
message User {
int64 id = 1;
string name = 2;
// 已废弃的字段
string old_field = 3 [deprecated = true];
// JSON 名称自定义
string full_name = 4 [json_name = "fullName"];
}
文件选项 #
syntax = "proto3";
package example.v1;
// Go 相关选项
option go_package = "github.com/example/proto/example/v1";
// Java 相关选项
option java_package = "com.example.proto.v1";
option java_outer_classname = "ExampleProto";
// 优化选项
option optimize_for = SPEED; // SPEED, CODE_SIZE, LITE_RUNTIME
服务定义 #
基本服务定义 #
service UserService {
// 一元 RPC
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// 服务端流式 RPC
rpc ListUsers(ListUsersRequest) returns (stream User);
// 客户端流式 RPC
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
// 双向流式 RPC
rpc ChatWithUsers(stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
int64 user_id = 1;
}
message GetUserResponse {
User user = 1;
bool found = 2;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message CreateUserRequest {
User user = 1;
}
message CreateUsersResponse {
repeated User users = 1;
int32 created_count = 2;
}
message ChatMessage {
int64 user_id = 1;
string message = 2;
int64 timestamp = 3;
}
方法选项 #
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/v1/users/{user_id}"
};
}
rpc CreateUser(CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}
}
代码生成 #
生成命令 #
# 基本生成命令
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/user.proto
# 生成到指定目录
protoc --go_out=./generated --go_opt=paths=source_relative \
--go-grpc_out=./generated --go-grpc_opt=paths=source_relative \
proto/*.proto
# 使用 Makefile 自动化
.PHONY: proto
proto:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/*.proto
生成的代码结构 #
生成的 Go 代码包含:
消息类型(.pb.go):
type User struct {
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
}
// Getter 方法
func (x *User) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
// 其他生成的方法...
服务接口(_grpc.pb.go):
// 服务端接口
type UserServiceServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
ListUsers(*ListUsersRequest, UserService_ListUsersServer) error
// ...
}
// 客户端接口
type UserServiceClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (UserService_ListUsersClient, error)
// ...
}
最佳实践 #
1. 字段编号管理 #
message User {
// 保留已删除的字段编号
reserved 4, 6 to 10;
reserved "old_name", "deprecated_field";
int64 id = 1;
string name = 2;
string email = 3;
// 字段 4 已保留
string phone = 5;
// 字段 6-10 已保留
string address = 11;
}
2. 向后兼容性 #
// 版本 1
message UserV1 {
int64 id = 1;
string name = 2;
}
// 版本 2 - 添加新字段
message UserV2 {
int64 id = 1;
string name = 2;
string email = 3; // 新增字段
repeated string tags = 4; // 新增重复字段
}
3. 命名约定 #
// 使用 snake_case 命名
message UserProfile {
int64 user_id = 1; // 不是 userId
string first_name = 2; // 不是 firstName
string last_name = 3; // 不是 lastName
repeated string phone_numbers = 4; // 不是 phoneNumbers
}
// 服务名使用 PascalCase
service UserManagementService {
// 方法名使用 PascalCase
rpc GetUserProfile(GetUserProfileRequest) returns (UserProfile);
rpc UpdateUserProfile(UpdateUserProfileRequest) returns (UserProfile);
}
4. 错误处理 #
import "google/rpc/status.proto";
message GetUserResponse {
oneof result {
User user = 1;
google.rpc.Status error = 2;
}
}
5. 分页处理 #
message ListUsersRequest {
int32 page_size = 1; // 页面大小
string page_token = 2; // 分页令牌
string filter = 3; // 过滤条件
string order_by = 4; // 排序字段
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2; // 下一页令牌
int32 total_count = 3; // 总数(可选)
}
性能优化 #
1. 字段顺序优化 #
// 优化前:大字段在前
message User {
string biography = 1; // 大字段
repeated string hobbies = 2; // 重复字段
int64 id = 3; // 小字段
string name = 4; // 中等字段
}
// 优化后:小字段在前
message User {
int64 id = 1; // 小字段在前
string name = 2; // 中等字段
repeated string hobbies = 3; // 重复字段
string biography = 4; // 大字段在后
}
2. 使用合适的数值类型 #
message OptimizedMessage {
// 对于小的正整数,使用 int32 而不是 int64
int32 count = 1;
// 对于经常为负数的字段,使用 sint32/sint64
sint32 temperature = 2;
// 对于固定长度的数据,使用 fixed32/fixed64
fixed64 timestamp_nanos = 3;
}
小结 #
本节深入介绍了 Protocol Buffers 的语法和特性:
- 基础语法:数据类型、消息定义、服务定义
- 高级特性:oneof、any、枚举、嵌套消息
- 代码生成:编译器使用、生成的代码结构
- 最佳实践:命名约定、向后兼容性、性能优化
Protocol Buffers 为 gRPC 提供了强大的类型系统和高效的序列化机制。在下一节中,我们将学习如何实现完整的 gRPC 服务端。