3.7.2 Protocol Buffers

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 的语法和特性:

  1. 基础语法:数据类型、消息定义、服务定义
  2. 高级特性:oneof、any、枚举、嵌套消息
  3. 代码生成:编译器使用、生成的代码结构
  4. 最佳实践:命名约定、向后兼容性、性能优化

Protocol Buffers 为 gRPC 提供了强大的类型系统和高效的序列化机制。在下一节中,我们将学习如何实现完整的 gRPC 服务端。