Skip to content

Protocol Buffers

Protocol Buffers(protobuf)是 Google 开发的一种语言无关、平台无关的序列化数据结构的方法,广泛用于通信协议、数据存储等场景。

📋 学习目标

完成本教程后,你将能够:

  • 理解 Protocol Buffers 的概念和优势
  • 掌握 .proto 文件的语法
  • 理解数据类型和字段规则
  • 学会定义消息和服务
  • 掌握 protoc 编译器的使用
  • 理解版本兼容性处理
  • 了解最佳实践

🎯 Protocol Buffers 简介

什么是 Protocol Buffers

Protocol Buffers 是一种灵活、高效、自动化的序列化结构化数据的机制。与 XML 和 JSON 相比,Protocol Buffers 更小、更快、更简单。

为什么选择 Protocol Buffers

  • 高效: 二进制格式,比 JSON/XML 更小更快
  • 跨语言: 支持多种编程语言
  • 类型安全: 强类型定义,编译时检查
  • 向后兼容: 支持字段添加和删除,保持兼容性
  • 代码生成: 自动生成序列化/反序列化代码

Protocol Buffers vs JSON

特性Protocol BuffersJSON
格式二进制文本
大小更小(约小 3-10 倍)较大
解析速度更快较慢
可读性需要工具查看人类可读
类型支持强类型弱类型
浏览器支持需要转换原生支持

🚀 快速开始

安装工具

bash
# 安装 Protocol Buffers 编译器
# macOS
brew install protobuf

# Linux
sudo apt-get install protobuf-compiler

# 验证安装
protoc --version

# 安装 Go 插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

第一个 .proto 文件

protobuf
// user.proto
syntax = "proto3";

package user;

option go_package = "./proto;user";

// 用户消息定义
message User {
  int64 id = 1;
  string name = 2;
  string email = 3;
  int32 age = 4;
  bool active = 5;
}

生成 Go 代码

bash
protoc --go_out=. --go_opt=paths=source_relative \
       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
       user.proto

使用生成的代码

go
package main

import (
	"fmt"
	"log"

	"google.golang.org/protobuf/proto"
	pb "your-project/proto"
)

func main() {
	// 创建消息
	user := &pb.User{
		Id:     1,
		Name:   "Alice",
		Email:  "alice@example.com",
		Age:    25,
		Active: true,
	}

	// 序列化
	data, err := proto.Marshal(user)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Serialized size: %d bytes\n", len(data))

	// 反序列化
	var newUser pb.User
	if err := proto.Unmarshal(data, &newUser); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("User: %+v\n", newUser)
}

📚 语法基础

文件结构

protobuf
syntax = "proto3";  // 指定语法版本

package mypackage;  // 包名,用于避免命名冲突

option go_package = "./proto;mypackage";  // Go 包路径

// 导入其他 .proto 文件
import "google/protobuf/timestamp.proto";

// 消息定义
message MyMessage {
  // 字段定义
}

// 服务定义
service MyService {
  // RPC 方法定义
}

字段编号

每个字段都有一个唯一的编号,用于在二进制格式中标识字段。编号一旦使用,就不应该更改。

protobuf
message User {
  int64 id = 1;        // 编号 1
  string name = 2;      // 编号 2
  string email = 3;     // 编号 3
}

重要规则

  • 编号范围:1 到 2^29-1(536,870,911)
  • 19000-19999 为 Protocol Buffers 保留,不能使用
  • 1-15 使用 1 字节编码,16-2047 使用 2 字节编码
  • 建议常用字段使用 1-15 的编号

🔢 数据类型

标量类型

.proto 类型Go 类型说明
doublefloat6464 位浮点数
floatfloat3232 位浮点数
int32int3232 位整数
int64int6464 位整数
uint32uint32无符号 32 位整数
uint64uint64无符号 64 位整数
sint32int32有符号 32 位整数(变长编码)
sint64int64有符号 64 位整数(变长编码)
fixed32uint32固定 32 位无符号整数
fixed64uint64固定 64 位无符号整数
sfixed32int32固定 32 位有符号整数
sfixed64int64固定 64 位有符号整数
boolbool布尔值
stringstringUTF-8 或 ASCII 字符串
bytes[]byte任意字节序列

示例

protobuf
message Example {
  double price = 1;
  float score = 2;
  int32 count = 3;
  int64 timestamp = 4;
  uint32 version = 5;
  bool enabled = 6;
  string name = 7;
  bytes data = 8;
}

📦 复合类型

枚举(Enum)

protobuf
message User {
  int64 id = 1;
  string name = 2;
  UserStatus status = 3;
}

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;  // 必须从 0 开始
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_INACTIVE = 2;
  USER_STATUS_SUSPENDED = 3;
}

嵌套消息

protobuf
message User {
  int64 id = 1;
  string name = 2;
  Address address = 3;  // 嵌套消息
}

message Address {
  string street = 1;
  string city = 2;
  string country = 3;
  int32 zip_code = 4;
}

数组(Repeated)

protobuf
message User {
  int64 id = 1;
  string name = 2;
  repeated string tags = 3;           // 字符串数组
  repeated int32 scores = 4;          // 整数数组
  repeated Address addresses = 5;     // 消息数组
}

在 Go 中,repeated 字段会生成为切片:

go
user := &pb.User{
	Tags: []string{"developer", "golang"},
	Scores: []int32{95, 87, 92},
	Addresses: []*pb.Address{
		{Street: "123 Main St", City: "New York"},
	},
}

Map

protobuf
message User {
  int64 id = 1;
  string name = 2;
  map<string, string> metadata = 3;      // 字符串到字符串的映射
  map<int32, Address> address_map = 4;    // 整数到消息的映射
}

在 Go 中的使用:

go
user := &pb.User{
	Metadata: map[string]string{
		"department": "Engineering",
		"role": "Senior Developer",
	},
}

🔄 字段规则

proto3 字段规则

在 proto3 中,所有字段默认都是可选的(optional),并且:

  • 标量类型有默认值(数字为 0,字符串为空,布尔值为 false)
  • 消息类型为 nil
  • repeated 字段为空切片
protobuf
message Example {
  int32 count = 1;              // 默认值: 0
  string name = 2;              // 默认值: ""
  bool active = 3;              // 默认值: false
  repeated int32 numbers = 4;   // 默认值: []
  Address address = 5;          // 默认值: nil
}

检查字段是否设置

在 proto3 中,无法区分字段是未设置还是设置为默认值。如果需要区分,可以使用 oneofwrapper 类型:

protobuf
import "google/protobuf/wrappers.proto";

message User {
  int64 id = 1;
  string name = 2;
  google.protobuf.Int32Value age = 3;  // 可以区分 nil 和 0
}

🎯 Oneof

oneof 允许你定义一组字段,其中只有一个字段会被设置:

protobuf
message User {
  int64 id = 1;
  string name = 2;

  oneof contact {
    string email = 3;
    string phone = 4;
  }
}

在 Go 中的使用:

go
user := &pb.User{
	Id: 1,
	Name: "Alice",
	Contact: &pb.User_Email{Email: "alice@example.com"},
	// 或
	// Contact: &pb.User_Phone{Phone: "123-456-7890"},
}

📝 服务定义

Protocol Buffers 可以定义 gRPC 服务:

protobuf
service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
  rpc CreateUser (CreateUserRequest) returns (User);
  rpc UpdateUser (UpdateUserRequest) returns (User);
  rpc DeleteUser (DeleteUserRequest) returns (google.protobuf.Empty);

  // 流式 RPC
  rpc StreamUsers (ListUsersRequest) returns (stream User);
}

🔧 高级特性

导入其他 .proto 文件

protobuf
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
import "common/user.proto";

message Order {
  int64 id = 1;
  common.User user = 2;
  google.protobuf.Timestamp created_at = 3;
}

使用 Well-Known Types

Protocol Buffers 提供了一些常用的类型:

protobuf
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/any.proto";
import "google/protobuf/struct.proto";

message Event {
  google.protobuf.Timestamp timestamp = 1;
  google.protobuf.Duration duration = 2;
  google.protobuf.Any data = 3;
  google.protobuf.Struct metadata = 4;
}

选项(Options)

protobuf
message User {
  option (my_option) = "value";

  int64 id = 1 [(validate.rules).int64.gt = 0];
  string email = 2 [(validate.rules).string.email = true];
}

保留字段

为了保持向后兼容,可以保留已删除的字段编号和名称:

protobuf
message User {
  int64 id = 1;
  string name = 2;

  reserved 3, 5, 7 to 10;        // 保留字段编号
  reserved "old_field", "deprecated_field";  // 保留字段名称
}

🔄 版本兼容性

添加新字段

可以安全地添加新字段,只要使用新的字段编号:

protobuf
// 旧版本
message User {
  int64 id = 1;
  string name = 2;
}

// 新版本 - 向后兼容
message User {
  int64 id = 1;
  string name = 2;
  string email = 3;  // 新字段
}

删除字段

不要直接删除字段,而是使用 reserved

protobuf
message User {
  int64 id = 1;
  // string old_field = 2;  // 已删除
  reserved 2;
  string name = 3;
}

字段规则变更

  • ✅ 可以添加新字段
  • ✅ 可以删除字段(使用 reserved)
  • ✅ 可以添加新的枚举值
  • ❌ 不要更改字段编号
  • ❌ 不要更改字段类型
  • ❌ 不要将 required 改为 optional(proto2)

🛠️ protoc 编译器

基本用法

bash
protoc [OPTIONS] PROTO_FILES

常用选项

bash
# 生成 Go 代码
protoc --go_out=. user.proto

# 指定输出路径
protoc --go_out=./generated user.proto

# 生成 gRPC 代码
protoc --go_out=. --go-grpc_out=. user.proto

# 指定导入路径
protoc --go_out=. \
       --go_opt=paths=source_relative \
       --go_opt=Muser.proto=github.com/user/proto \
       user.proto

# 从多个目录导入
protoc --proto_path=./proto \
       --proto_path=./third_party \
       --go_out=. \
       user.proto

Makefile 示例

makefile
.PHONY: proto
proto:
	protoc --go_out=. --go_opt=paths=source_relative \
	       --go-grpc_out=. --go-grpc_opt=paths=source_relative \
	       --proto_path=. \
	       proto/*.proto

.PHONY: proto-clean
proto-clean:
	rm -rf proto/*.pb.go

💡 最佳实践

1. 命名规范

  • 消息名使用 PascalCase:User, GetUserRequest
  • 字段名使用 snake_case:user_id, created_at
  • 服务名使用 PascalCase:UserService
  • RPC 方法名使用 PascalCase:GetUser, CreateUser
  • 枚举值使用 UPPER_SNAKE_CASE:USER_STATUS_ACTIVE

2. 字段编号

  • 常用字段使用 1-15(编码更高效)
  • 不要重用已删除的字段编号
  • 为未来扩展预留编号范围

3. 包组织

proto/
├── user/
│   ├── user.proto
│   └── user_service.proto
├── order/
│   ├── order.proto
│   └── order_service.proto
└── common/
    └── common.proto

4. 版本管理

  • 使用语义化版本控制
  • 保持向后兼容
  • 使用 reserved 标记删除的字段

5. 文档注释

protobuf
// User 表示系统中的用户
message User {
  // 用户的唯一标识符
  int64 id = 1;

  // 用户的显示名称
  string name = 2;

  // 用户的电子邮件地址
  string email = 3;
}

📝 实践练习

  1. 基础练习:定义一个包含各种数据类型的消息
  2. 嵌套练习:创建包含嵌套消息和数组的复杂消息
  3. 服务练习:定义一个完整的用户服务,包含 CRUD 操作
  4. 版本升级练习:练习添加和删除字段,保持兼容性
  5. 综合练习:设计一个电商系统的消息定义

🔗 相关资源

⏭️ 下一步

完成 Protocol Buffers 学习后,可以继续学习:


🎉 恭喜! 你已经掌握了 Protocol Buffers 的基础知识。现在可以使用它来定义高效的数据结构和服务接口了!

基于 VitePress 构建