google protobuf3中文手册

简介: 本文介绍 Protocol Buffers proto3 语言的使用规范,涵盖 `.proto` 文件语法、消息与字段定义、枚举、映射、服务接口及代码生成等内容,指导开发者正确设计和更新数据结构,确保跨语言兼容性与二进制安全。

语言指南(proto3)

本文档介绍如何在项目中使用 Protocol Buffers 语言的 proto3 版本。

本指南将说明如何使用 Protocol Buffers 语言定义 Protocol Buffers 数据结构,包括 .proto 文件语法以及如何从 .proto 文件生成数据访问类。内容涵盖 Protocol Buffers 语言的 proto3 版本。

  • 有关版本语法(Editions Syntax)的信息,请参阅《Protobuf 版本语言指南》(Protobuf Editions Language Guide)。
  • 有关 proto2 语法的信息,请参阅《Proto2 语言指南》(Proto2 Language Guide)。

本文档为参考指南——若需逐步实践本文档中描述的多个特性,可查看对应编程语言的教程。

定义消息类型

首先来看一个简单示例。假设需要定义搜索请求的消息格式,每个搜索请求包含查询字符串、目标结果页号和每页结果数量。以下是用于定义该消息类型的 .proto 文件:

syntax = "proto3";

message SearchRequest {
  string query = 1;        // 查询字符串
  int32 page_number = 2;   // 目标结果页号(从1开始)
  int32 results_per_page = 3; // 每页结果数量
}

核心说明

  1. 语法声明:文件第一行指定使用 proto3 语法规范。版本声明(或 proto2/proto3 的语法声明)必须是文件中第一个非空、非注释行。若未指定版本或语法,Protocol Buffers 编译器将默认使用 proto2。
  2. 消息定义SearchRequest 消息定义包含三个字段(名称/值对),对应需要包含的三类数据。每个字段均包含名称和类型。

指定字段类型

上述示例中所有字段均为标量类型:两个整数(page_numberresults_per_page)和一个字符串(query)。此外,还可以指定枚举类型和复合类型(如其他消息类型)作为字段类型。

分配字段编号

必须为消息定义中的每个字段分配一个 1 到 536,870,911 之间的编号,并遵守以下规则:

  1. 字段编号在同一消息内必须唯一。
  2. 19,000 到 19,999 之间的字段编号为 Protocol Buffers 实现预留,若在消息中使用该范围编号,编译器会报错。
  3. 不能使用之前预留的字段编号或已分配给扩展的字段编号。
  4. 消息类型投入使用后,字段编号不可修改——因为它在消息的二进制编码格式中用于标识字段。修改字段编号等同于删除该字段并创建一个相同类型但编号不同的新字段(详见「删除字段」章节)。
  5. 字段编号不可重复使用,切勿从预留列表中取出编号用于新字段定义(详见「重复使用字段编号的后果」)。

编号优化建议

  • 频繁设置的字段应使用 1-15 之间的编号:该范围的编号在二进制编码中仅占 1 字节。
  • 16-2047 之间的编号占 2 字节,适用于不常用字段。
  • 更多编码细节可参考《Protocol Buffer 编码》(Protocol Buffer Encoding)。

重复使用字段编号的后果

重复使用字段编号会导致二进制消息解码歧义,因为 Protocol Buffers 二进制格式紧凑,无法检测字段是用旧定义编码还是新定义编码。可能引发以下问题:

  • 开发人员调试耗时增加
  • 解析/合并错误(最佳情况)
  • 个人身份信息(PII)/敏感个人信息(SPII)泄露
  • 数据损坏

常见原因

  1. 重新编号字段(有时为了使字段编号顺序更美观):本质上是删除并重新添加所有涉及的字段,导致二进制格式不兼容。
  2. 删除字段但未预留其编号,导致后续被重复使用。

字段编号限制为 29 位(而非 32 位),因为 3 位用于指定字段的编码格式(详见「编码」章节)。

指定字段基数

消息字段的基数(Cardinality)可分为以下类型:

1. 单数字段(Singular)

proto3 中有两种单数字段:

(1)optional(推荐)

可选字段有两种状态:

  • 已设置:包含显式设置或从二进制数据解析的值,会被序列化到二进制流。
  • 未设置:访问时返回默认值,不会被序列化到二进制流。
    可通过代码检查字段是否被显式设置。

推荐使用 optional 而非隐式字段,以最大限度兼容 Protobuf 版本(Editions)和 proto2。

(2)implicit(不推荐)

无显式基数标签的字段,行为如下:

  • 若字段为消息类型,行为与 optional 一致。
  • 若字段为非消息类型,有两种状态:
    • 已设置为非默认值(非零):会被序列化到二进制流。
    • 已设置为默认值(零):不会被序列化到二进制流,且无法区分该值是显式设置、从二进制解析还是未提供(详见「字段存在性」Field Presence)。

2. repeated

该类型字段在合法消息中可重复 0 次或多次,重复值的顺序会被保留。

3. map

键值对字段类型(详见「映射」章节)。

重复字段默认启用打包编码

proto3 中,标量数值类型的重复字段默认使用打包编码(packed encoding),更多细节可参考《Protocol Buffer 编码》。

消息类型字段始终具有字段存在性

proto3 中,消息类型字段本身已支持字段存在性检查,因此添加 optional 修饰符不会改变其存在性行为。以下示例中 Message2Message3 的生成代码(所有语言)完全相同,且在二进制、JSON 和文本格式中的表示也无差异:

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1; // 隐式消息字段
}

message Message3 {
  optional Message1 bar = 1; // 显式 optional 消息字段
}

合法消息

Protocol Buffers 中的「合法消息」指可正确序列化/反序列化的二进制数据,protoc 解析器会验证 .proto 定义文件是否可解析。

  • 单数字段在二进制数据中可出现多次:解析器会接受输入,但生成的代码仅能访问该字段的最后一个实例(详见「最后一个生效」Last One Wins)。

添加更多消息类型

单个 .proto 文件中可定义多个消息类型,适用于相关消息的分组。例如,为 SearchRequest 定义对应的响应消息 SearchResponse

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

message SearchResponse {
  // 响应字段定义
}

注意:虽然单个 .proto 文件可定义多个消息、枚举或服务,但大量依赖不同的消息集中在一个文件会导致依赖膨胀。建议每个 .proto 文件仅包含尽可能少的消息类型。

添加注释

.proto 文件支持两种注释风格:

  1. 推荐使用 C/C++/Java 风格的行尾注释 //(位于代码元素上方)。
  2. 也支持 C 风格的多行注释 /* ... */,多行注释建议使用 * 作为边缘线。

示例:

/**
 * SearchRequest 表示搜索查询,包含分页选项以指定响应中应包含的结果。
 */
message SearchRequest {
  string query = 1;

  // 目标结果页号(从1开始)
  int32 page_number = 2;

  // 每页结果数量(默认10)
  int32 results_per_page = 3;
}

删除字段

若删除字段的操作不当,可能导致严重问题。正确流程如下:

  1. 确保客户端代码中已删除对该字段的所有引用。
  2. 从消息中删除字段定义。
  3. 必须预留该字段的编号和名称
    • 预留编号可防止后续重复使用。
    • 预留名称可确保消息的 JSON 和文本格式编码仍能正常解析。

预留字段编号

删除字段后,需将其编号添加到 reserved 列表,避免后续被重复使用。编译器会阻止未来开发者使用这些编号:

message Foo {
  reserved 2, 15, 9 to 11; // 单个编号或编号范围(含首尾)
}

预留字段名称

重复使用旧字段名称通常是安全的,但在文本格式(TextProto)或 JSON 编码中可能出现问题。可将删除的字段名称添加到 reserved 列表:

message Foo {
  reserved 2, 15, 9 to 11; // 预留编号
  reserved "foo", "bar";    // 预留名称
}

注意:同一 reserved 语句中不能混合字段编号和名称。

.proto 文件生成的内容

运行 Protocol Buffers 编译器(protoc)处理 .proto 文件时,会根据指定的编程语言生成对应的代码,用于操作消息类型,包括字段的读写、消息的序列化(输出流)和反序列化(输入流)。

各语言生成文件说明

编程语言 生成文件及内容
C++ 每个 .proto 生成 .h.cc 文件,包含每个消息类型对应的类
Java 生成 .java 文件,包含每个消息类型的类及用于创建实例的 Builder 类
Kotlin 除 Java 生成代码外,额外生成 .kt 文件,提供优化的 Kotlin API(含 DSL、可空字段访问器、复制函数)
Python 生成包含消息类型静态描述符的模块,运行时通过元类创建数据访问类
Go 生成 .pb.go 文件,包含每个消息类型对应的结构体
Ruby 生成 .rb 文件,包含消息类型的 Ruby 模块
Objective-C 每个 .proto 生成 pbobjc.hpbobjc.m 文件,包含每个消息类型的类
C# 生成 .cs 文件,包含每个消息类型的类
PHP 为每个消息类型生成 .php 消息文件,为每个 .proto 生成 .php 元数据文件(用于加载消息类型到描述符池)
Dart 生成 .pb.dart 文件,包含每个消息类型的类

进一步学习

  • 各语言 API 使用方法可参考对应编程语言的教程。
  • 详细 API 文档可查看相关语言的 API 参考。

标量值类型

标量消息字段支持以下类型,表格列出了 .proto 中指定的类型及自动生成类中的对应类型:

标量类型说明

Proto 类型 说明
double 使用 IEEE 754 双精度浮点数格式
float 使用 IEEE 754 单精度浮点数格式
int32 可变长度编码,对负数编码效率低(若字段可能为负数,建议使用 sint32)
int64 可变长度编码,对负数编码效率低(若字段可能为负数,建议使用 sint64)
uint32 可变长度编码
uint64 可变长度编码
sint32 可变长度编码,带符号整数,对负数编码效率高于 int32
sint64 可变长度编码,带符号整数,对负数编码效率高于 int64
fixed32 固定 4 字节,若值经常大于 2²⁸,效率高于 uint32
fixed64 固定 8 字节,若值经常大于 2⁵⁶,效率高于 uint64
sfixed32 固定 4 字节,带符号整数
sfixed64 固定 8 字节,带符号整数
bool 布尔类型
string 必须包含 UTF-8 编码或 7 位 ASCII 文本,长度不超过 2³²
bytes 可包含任意字节序列,长度不超过 2³²

各语言对应类型表

Proto 类型 C++ 类型 Java/Kotlin 类型 Python 类型 Go 类型 Ruby 类型 C# 类型 PHP 类型 Dart 类型 Rust 类型
double double double float float64 Float double float double f64
float float float float float32 Float float float double f32
int32 int32_t int int int32 Fixnum/Bignum int integer int i32
int64 int64_t long int/long int64 Bignum long integer/string Int64 i64
uint32 uint32_t int int/long uint32 Fixnum/Bignum uint integer int u32
uint64 uint64_t long int/long uint64 Bignum ulong integer/string Int64 u64
sint32 int32_t int int int32 Fixnum/Bignum int integer int i32
sint64 int64_t long int/long int64 Bignum long integer/string Int64 i64
fixed32 uint32_t int int/long uint32 Fixnum/Bignum uint integer int u32
fixed64 uint64_t long int/long uint64 Bignum ulong integer/string Int64 u64
sfixed32 int32_t int int int32 Fixnum/Bignum int integer int i32
sfixed64 int64_t long int/long int64 Bignum long integer/string Int64 i64
bool bool boolean bool bool TrueClass/FalseClass bool boolean bool bool
string std::string String str/unicode string String (UTF-8) string string String ProtoString
bytes std::string ByteString str (Python 2)/bytes (Python 3) []byte String (ASCII-8BIT) ByteString string List ProtoBytes

补充说明

  1. Kotlin 使用 Java 对应的类型(包括无符号类型),以确保 Java/Kotlin 混合项目的兼容性。
  2. Java 中,无符号 32 位和 64 位整数通过有符号对应类型表示,最高位存储在符号位中。
  3. 所有语言中,设置字段值时都会进行类型检查,确保合法性。
  4. 64 位或无符号 32 位整数解码时始终表示为 long,但设置字段时可传入 int(需确保值在对应类型范围内)。
  5. Python 字符串解码时表示为 unicode,若传入 ASCII 字符串可表示为 str(可能变更)。
  6. PHP 中,64 位机器使用 integer 类型,32 位机器使用 string 类型。

编码细节

标量类型的序列化编码规则可参考《Protocol Buffer 编码》。

字段默认值

解析消息时,若二进制数据中未包含某个字段,访问该字段会返回其默认值。默认值因类型而异:

  • 字符串:空字符串(""
  • 字节:空字节序列(""
  • 布尔值:false
  • 数值类型:0
  • 消息字段:未设置,具体行为因语言而异(详见对应语言的生成代码指南)
  • 枚举:第一个定义的枚举值(必须为 0,详见「枚举默认值」)
  • 重复字段:空列表(各语言对应的数据结构)
  • 映射字段:空映射(各语言对应的数据结构)

注意事项

  1. 对于隐式存在性标量字段,无法区分字段是显式设置为默认值(如布尔值 false)还是未设置,定义消息类型时需注意。例如,若不希望默认触发某个行为,不应使用布尔字段的 false 来控制该行为。
  2. 标量字段设置为默认值时,不会被序列化到二进制流。+0 不会被序列化,但 -0 被视为不同值,会被序列化。
  3. 各语言默认值的具体实现可参考对应语言的生成代码指南。

枚举

定义消息类型时,若希望某个字段仅能取预定义的一组值,可使用枚举(enum)类型。例如,为 SearchRequest 添加 corpus 字段,指定搜索范围为 UNIVERSAL、WEB、IMAGES 等:

// 定义枚举类型 Corpus
enum Corpus {
  CORPUS_UNSPECIFIED = 0; // 默认值(必须为 0)
  CORPUS_UNIVERSAL = 1;   // 通用搜索
  CORPUS_WEB = 2;         // 网页搜索
  CORPUS_IMAGES = 3;      // 图片搜索
  CORPUS_LOCAL = 4;       // 本地搜索
  CORPUS_NEWS = 5;        // 新闻搜索
  CORPUS_PRODUCTS = 6;    // 商品搜索
  CORPUS_VIDEO = 7;       // 视频搜索
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4; // 使用枚举类型作为字段
}

枚举值前缀规范

枚举值前缀应确保去掉前缀后的名称仍为合法且符合风格的枚举名。例如:

  • 不推荐:DEVICE_TIER_1(去掉前缀 DEVICE_TIER_ 后为 1,不合法)
  • 推荐:DEVICE_TIER_TIER1(去掉前缀后为 TIER1,合法)

部分 Protobuf 实现会自动去除与枚举名匹配的前缀(若安全),但上述不推荐示例中无法去除。未来版本计划支持作用域枚举(scoped enums),无需手动添加前缀,可直接写 TIER1 = 1

枚举默认值

  • 枚举的默认值为第一个定义的枚举值(必须为 0),如上述示例中的 CORPUS_UNSPECIFIED
  • 规则要求:
    1. 必须存在值为 0 的枚举项(作为数值默认值)。
    2. 0 值枚举项必须是第一个元素(兼容 proto2 语义,proto2 中默认使用第一个枚举值,除非显式指定)。
  • 建议:第一个默认值仅表示「未指定」,无其他语义。

枚举值别名

通过为不同枚举常量分配相同值,可定义别名。需设置 allow_alias = true,否则编译器会报警告。序列化时所有别名均有效,但反序列化时仅使用第一个定义的别名:

// 允许别名的枚举
enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1; // 别名(与 EAA_STARTED 等价)
  EAA_FINISHED = 2;
}

// 不允许别名的枚举
enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1; // 取消注释会触发警告
  ENAA_FINISHED = 2;
}

枚举值范围与复用

  • 枚举常量的值必须在 32 位整数范围内,不推荐使用负数(varint 编码效率低)。
  • 枚举可定义在消息内部或外部,外部枚举可在同一 .proto 文件的任意消息中复用。
  • 可使用 _消息类型_._枚举类型_ 引用其他消息中定义的枚举,如 MessageA.EnumB

生成代码与未识别枚举值处理

  • 编译器会为枚举生成对应语言的枚举类型(如 Java/C++/Kotlin)或枚举描述符类(如 Python)。
  • 注意:部分语言对枚举项数量有限制(数千个),需查阅目标语言的限制说明。
  • 反序列化时,未识别的枚举值会被保留,但表示方式因语言而异:
    • 支持开放枚举类型的语言(如 C++、Go):直接存储为底层整数值。
    • 支持封闭枚举类型的语言(如 Java):使用特殊枚举项表示未识别值,可通过访问器获取底层整数。
  • 未识别的枚举值在序列化时会被保留。

有关枚举的预期行为与各语言实际行为的差异,可参考《枚举行为》(Enum Behavior)。

预留枚举值

删除枚举项后,需将其数值和/或名称添加到 reserved 列表,防止后续重复使用(避免数据损坏、隐私泄露等问题)。可使用 max 表示数值范围的上限:

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max; // 预留数值(单个或范围)
  reserved "FOO", "BAR"; // 预留名称
}

注意:同一 reserved 语句中不能混合数值和名称。

使用其他消息类型

可将其他消息类型作为字段类型。例如,在 SearchResponse 中包含 Result 消息:

// 搜索响应消息,包含多个结果
message SearchResponse {
  repeated Result results = 1; // 重复字段,类型为 Result
}

// 单个搜索结果消息
message Result {
  string url = 1;        // 结果 URL
  string title = 2;      // 结果标题
  repeated string snippets = 3; // 结果摘要(支持多个)
}

导入定义

若需使用其他 .proto 文件中定义的消息类型,可通过 import 语句导入:

import "myproject/other_protos.proto"; // 导入其他 .proto 文件

导入路径解析

  • 编译器通过 -I/--proto_path 标志指定的目录搜索导入文件,导入路径为相对于这些目录的路径。
  • 示例目录结构:
    my_project/
    ├── protos/
    │   ├── main.proto       // 主文件
    │   └── common/
    │       └── timestamp.proto // 待导入文件
    
  • 编译命令(从 my_project 目录执行):
    protoc --proto_path=protos protos/main.proto --go_out=./gen
    
  • main.proto 中的导入语句:
    import "common/timestamp.proto"; // 相对于 --proto_path 指定的 protos 目录
    

公共导入(Import Public)

当需要移动 .proto 文件时,可使用 import public 实现导入转发,避免直接修改所有引用方。例如,将 old.proto 的内容迁移到 new.proto 后:

// new.proto:包含原 old.proto 的所有定义
message NewMessage { ... }

// old.proto:转发导入到 new.proto
import public "new.proto"; // 公共导入,引用 old.proto 的文件可间接使用 new.proto 的定义
import "other.proto";      // 普通导入,仅 old.proto 可使用

// client.proto:导入 old.proto 后,可使用 new.proto 的定义,但不能使用 other.proto 的定义
import "old.proto";

注意:Java 中 import public 仅在移动整个 .proto 文件或启用 java_multiple_files = true 时效果最佳;Kotlin、TypeScript、JavaScript、GCL 及使用 Protobuf 静态反射的 C++ 目标不支持该功能。

导入 proto2 消息类型

  • 支持在 proto3 消息中导入并使用 proto2 消息类型,反之亦然。
  • 限制:proto3 语法中不能直接使用 proto2 枚举类型(但导入的 proto2 消息中使用该枚举是允许的)。

嵌套类型

可在消息内部定义嵌套消息,支持多层嵌套。嵌套消息可通过 _父消息_._子消息_ 在外部复用:

示例 1:简单嵌套

message SearchResponse {
  // 嵌套在 SearchResponse 内部的 Result 消息
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1; // 使用嵌套消息作为字段类型
}

// 外部消息引用嵌套类型
message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

示例 2:深层嵌套与独立命名空间

message Outer {       // 层级 0
  message MiddleAA {  // 层级 1
    message Inner {   // 层级 2(与 MiddleBB.Inner 独立)
      int64 ival = 1;
      bool booly = 2;
    }
  }
  message MiddleBB {  // 层级 1
    message Inner {   // 层级 2(与 MiddleAA.Inner 独立)
      int32 ival = 1;
      bool booly = 2;
    }
  }
}

更新消息类型

当现有消息类型需要扩展(如添加字段)但仍需兼容旧代码时,可遵循以下规则安全更新(基于二进制编码格式)。若使用 ProtoJSON 或文本格式存储消息,更新规则不同(详见《ProtoJSON 安全更新》)。

核心原则

参考《Protobuf 最佳实践》(Proto Best Practices),更新分为以下三类:

1. 二进制不兼容更新(Wire-unsafe Changes)

此类更新会导致新旧 schema 解析失败,仅当所有序列化/反序列化端均使用新 schema 时才可执行:

  • 修改现有字段的编号。
  • 将字段移入已存在的 oneof。

2. 二进制兼容更新(Wire-safe Changes)

此类更新不会导致数据丢失或解析失败,但可能影响应用代码(如枚举新增值导致 exhaustive switch 编译失败):

  • 添加新字段(旧代码解析时会忽略新字段,新代码解析旧数据时使用默认值)。
  • 删除字段(需预留字段编号和名称,避免重复使用)。
  • 为枚举添加新值。
  • 将单个显式存在性字段或扩展转换为新 oneof 的成员。
  • 将仅含一个字段的 oneof 转换为显式存在性字段。
  • 将字段转换为相同编号和类型的扩展。

3. 二进制条件兼容更新(Wire-compatible Changes)

此类更新可解析相同数据,但可能存在数据丢失风险,需谨慎部署:

  • 整数类型兼容(int32/uint32/int64/uint64/bool 相互兼容):超出目标类型范围的值会被截断(如 int64 转 int32 会丢失高位)。
  • sint32 与 sint64 相互兼容,但与其他整数类型不兼容:超出范围的值会导致解码错误。
  • string 与 bytes 兼容(需确保 bytes 为合法 UTF-8 编码)。
  • 嵌入消息与 bytes 兼容(bytes 需为该消息的合法编码)。
  • fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
  • 单数字段与重复字段(string/bytes/消息类型)兼容:
    • 重复字段转单数字段:取最后一个值(原始类型)或合并所有值(消息类型)。
    • 注意:数值类型的重复字段默认使用打包编码,单数字段解析时可能失败。
  • 枚举与整数类型(int32/uint32/int64/uint64)兼容:未识别的枚举值会被保留。
  • map 与对应的重复消息字段兼容(详见「映射」章节):
    • 重复消息字段转 map:可能丢失重复键(保留最后一个)。
    • map 转重复消息字段:键值对顺序不确定。

未知字段

未知字段指解析器无法识别的、格式合法的 Protocol Buffers 序列化数据(如旧代码解析含新字段的消息时,新字段即为未知字段)。

未知字段的保留

proto3 消息会保留未知字段,并在序列化时包含它们(与 proto2 行为一致)。但以下操作会导致未知字段丢失:

  • 将 proto 序列化为 JSON。
  • 遍历消息所有字段并复制到新消息(字段级复制)。

避免未知字段丢失的建议

  1. 使用二进制格式进行数据交换,避免文本格式。
  2. 使用消息级 API(如 CopyFrom()MergeFrom())复制数据,而非字段级复制。

注意:TextFormat 会以字段编号显示未知字段,但解析含未知字段编号的 TextFormat 数据时会失败。

Any 类型

Any 类型允许嵌入未指定 .proto 定义的消息。它包含任意序列化消息的字节数据和一个全局唯一的类型 URL(用于标识消息类型)。使用前需导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1; // 错误描述
  repeated google.protobuf.Any details = 2; // 附加详情(任意消息类型)
}

类型 URL 格式

默认类型 URL 为 type.googleapis.com/_包名_._消息名_,例如 type.googleapis.com/foo.bar.User

打包与解包

各语言运行时库提供类型安全的打包(pack)和解包(unpack)工具。示例(Java/C++):

// Java:打包 NetworkErrorDetails 到 Any
NetworkErrorDetails details = NetworkErrorDetails.newBuilder().build();
ErrorStatus status = ErrorStatus.newBuilder()
    .addDetails(Any.pack(details))
    .build();

// Java:从 Any 解包 NetworkErrorDetails
for (Any detail : status.getDetailsList()) {
   
  if (detail.is(NetworkErrorDetails.class)) {
   
    NetworkErrorDetails networkError = detail.unpack(NetworkErrorDetails.class);
    // 处理 networkError
  }
}
// C++:打包 NetworkErrorDetails 到 Any
NetworkErrorDetails details;
ErrorStatus status;
status.add_details()->PackFrom(details);

// C++:从 Any 解包 NetworkErrorDetails
for (const google::protobuf::Any& detail : status.details()) {
   
  if (detail.Is<NetworkErrorDetails>()) {
   
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    // 处理 network_error
  }
}

Oneof 类型

若消息中有多个单数字段且最多同时设置一个,可使用 oneof 特性强制该行为并节省内存。oneof 中的所有字段共享内存,设置一个字段会自动清空其他字段。可通过语言特定的 case()WhichOneof() 方法检查当前设置的字段。

注意:若设置多个 oneof 字段,仅最后设置的字段有效。oneof 字段的编号在所属消息中必须唯一。

定义 Oneof

使用 oneof 关键字定义,后跟 oneof 名称,字段类型支持任意类型(map 和 repeated 除外,若需重复字段可嵌套消息):

message SampleMessage {
  oneof test_oneof { // oneof 名称:test_oneof
    string name = 4;       // 字符串字段
    SubMessage sub_message = 9; // 消息类型字段
  }
}

Oneof 特性

  1. 自动清空其他字段:设置 oneof 中的某个字段时,其他字段会被清空。
    SampleMessage message;
    message.set_name("Alice");
    message.mutable_sub_message(); // 清空 name 字段
    assert(message.name().empty()); // 成立
    
  2. 解析规则:二进制数据中若存在多个 oneof 字段,仅最后一个有效。解析时:
    • 若当前已设置其他 oneof 字段,先清空。
    • 原始类型字段:覆盖现有值。
    • 消息类型字段:合并现有值。
  3. 不可重复oneof 字段不能标记为 repeated
  4. 反射支持:反射 API 可用于 oneof 字段。
  5. 默认值序列化oneof 字段设置为默认值(如 int32 设为 0)时,会被标记为已设置并序列化。
  6. 内存安全(C++):设置 oneof 字段后,之前获取的其他字段指针会失效(可能导致崩溃)。
  7. 交换行为(C++):交换两个含 oneof 的消息时,oneof 的状态会互换。

兼容性注意事项

  1. 添加/删除 oneof 字段时,若检查 oneof 状态为 None/NOT_SET,无法区分是未设置还是设置了旧版本中的 oneof 字段(未知字段无法关联到 oneof)。
  2. 标签复用问题
    • 将单数字段移入/移出 oneof:可能导致序列化/解析后数据丢失(部分字段被清空)。
    • 删除后重新添加 oneof 字段:可能清空当前设置的字段。
    • 拆分/合并 oneof:类似字段移动,存在数据丢失风险。

映射(Maps)

Protocol Buffers 提供快捷语法定义关联映射(键值对):

map<key_type, value_type> map_field = N;

映射规则

  • 键类型(key_type):支持整数类型(int32、uint32 等)或字符串类型,不支持浮点数、bytes、枚举或消息类型。
  • 值类型(value_type):支持任意类型,不支持其他映射。
  • 示例
    message Project { string name = 1; }
    message User {
      map<string, Project> projects = 3; // 字符串键 -> Project 消息值
    }
    

映射特性

  1. 不可重复:映射字段不能标记为 repeated
  2. 顺序不确定:二进制编码和迭代时,映射项的顺序不保证。
  3. 文本格式排序:生成文本格式时,映射项按键排序(数值键按数值排序)。
  4. 重复键处理
    • 二进制解析/合并:保留最后一个键值对。
    • 文本格式解析:重复键会导致解析失败。
  5. 缺省值序列化:仅提供键未提供值时,序列化行为因语言而异(C++/Java/Kotlin/Python 序列化默认值,其他语言不序列化)。
  6. 命名冲突:同一作用域中不能存在与映射字段同名的 FooEntry 消息(映射实现会自动生成该名称的消息)。

兼容性

映射语法在二进制层面等价于以下定义,因此不支持映射的 Protocol Buffers 实现仍可处理映射数据:

// 映射的二进制等价定义
message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

支持映射的实现必须能生成和解析上述格式的数据。

包(Packages)

可通过 package 关键字为 .proto 文件指定包名,避免消息类型命名冲突:

package foo.bar; // 包名

message Open { ... }

包的使用

引用其他包中的消息类型时,需指定完整包名:

message Foo {
  foo.bar.Open open = 1; // 引用 foo.bar 包中的 Open 消息
}

各语言对包的处理

编程语言 包名影响
C++ 生成的类位于 foo::bar 命名空间
Java/Kotlin 包名作为 Java 包名(可通过 java_package 选项覆盖)
Python 忽略包名(依赖文件系统组织模块)
Go 忽略包名,生成的 .pb.go 文件的包名由 go_proto_library 规则或 go_package 选项指定
Ruby 生成嵌套命名空间(如 Foo::Bar),名称首字母大写
PHP 包名转换为 PascalCase 命名空间(可通过 php_namespace 选项覆盖)
C# 包名转换为 PascalCase 命名空间(可通过 csharp_namespace 选项覆盖)

即使包名不直接影响生成代码(如 Python),仍建议为 .proto 文件指定包名,避免描述符命名冲突,确保跨语言兼容性。

包与名称解析

Protocol Buffers 中的类型名称解析类似 C++:从最内层作用域开始搜索,依次向外层扩展,每个包视为其父包的内层。前缀 . 表示从最外层作用域开始搜索(如 .foo.bar.Baz)。

编译器通过解析导入的 .proto 文件解析所有类型名称,各语言的代码生成器会处理不同的作用域规则。

定义服务(Services)

若需将消息类型用于 RPC(远程过程调用)系统,可在 .proto 文件中定义 RPC 服务接口。编译器会为目标语言生成服务接口代码和存根(stub)。

服务定义示例

// 定义搜索服务
service SearchService {
  // RPC 方法:输入 SearchRequest,输出 SearchResponse
  rpc Search(SearchRequest) returns (SearchResponse);
}

推荐 RPC 系统

  • gRPC:与 Protocol Buffers 配合最佳的开源 RPC 系统,支持多语言和跨平台,可通过编译器插件直接从 .proto 文件生成 RPC 代码。
  • 自定义 RPC 实现:若不使用 gRPC,可参考《Proto2 语言指南》实现自定义 RPC 系统。
  • 第三方 RPC 项目:可查看官方 Wiki 的「第三方插件」页面(third-party add-ons)。

JSON 映射

Protocol Buffers 二进制格式是系统间通信的首选序列化格式。对于需与 JSON 系统交互的场景,Protobuf 支持规范的 JSON 编码(详见《Protobuf JSON 映射》)。

选项(Options)

.proto 文件中的声明(消息、字段、枚举等)可通过选项(options)进行注解。选项不改变声明的核心含义,但会影响特定场景下的处理方式。所有可用选项定义在 google/protobuf/descriptor.proto 中。

选项作用域

  • 文件级选项:位于顶层(消息、枚举、服务之外)。
  • 消息级选项:位于消息定义内部。
  • 字段级选项:位于字段定义内部。
  • 其他作用域:枚举类型、枚举值、oneof 字段、服务类型、服务方法(目前无常用选项)。

常用选项

1. 文件级选项

选项 说明
java_package 指定生成的 Java/Kotlin 类的包名(默认使用 proto 包名),非 Java/Kotlin 代码无影响。示例:option java_package = "com.example.foo";
java_outer_classname 指定 Java 外层类名(默认将 .proto 文件名转为驼峰式),非 Java 代码无影响。示例:option java_outer_classname = "Ponycopter";
java_multiple_files 布尔值,控制是否为每个顶层消息/枚举/服务生成单独的 .java 文件(默认 false,所有类嵌套在外部类中)。示例:option java_multiple_files = true;
optimize_for 控制 C++/Java 代码生成优化方向:
- SPEED(默认):生成高效序列化/解析代码。
- CODE_SIZE:生成最小化代码(依赖反射,速度较慢)。
- LITE_RUNTIME:生成依赖精简运行时库(libprotobuf-lite)的代码(无反射,适用于移动端)。
示例:option optimize_for = CODE_SIZE;
cc_generic_services/java_generic_services/py_generic_services 布尔值,控制是否生成通用服务代码(已废弃,建议使用 RPC 插件生成特定代码),默认 true。示例:option cc_generic_services = false;
cc_enable_arenas 布尔值,启用 C++ 生成代码的内存池分配。
objc_class_prefix 指定 Objective-C 类前缀(3-5 个大写字母,Apple 保留 2 字母前缀)。示例:option objc_class_prefix = "FOO";

2. 字段级选项

选项 说明
packed 布尔值,控制重复标量数值字段是否使用打包编码(默认 true),用于兼容 2.3.0 之前的解析器。示例:repeated int32 samples = 4 [packed = false];
deprecated 布尔值,标记字段已废弃(新代码不应使用)。Java 中生成 @Deprecated 注解,C++ 中 clang-tidy 会报警告。示例:int32 old_field = 6 [deprecated = true];

枚举值选项

支持为枚举值添加选项(如 deprecated),也可通过扩展定义自定义选项:

import "google/protobuf/descriptor.proto";

// 定义自定义枚举值选项
extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true]; // 标记为废弃
  DATA_DISPLAY = 2 [(string_name) = "display_value"]; // 自定义选项
}

自定义选项

Protocol Buffers 支持定义和使用自定义选项(高级特性),需通过扩展实现。proto3 中仅允许为自定义选项使用扩展,具体方法可参考《Proto2 语言指南》。

选项保留(Option Retention)

选项的保留策略控制其是否保留在生成代码中:

  • 默认为 RETENTION_RUNTIME:保留在生成代码中,可通过运行时描述符池访问。
  • RETENTION_SOURCE:仅在源代码中保留,不进入运行时(减少二进制体积)。

示例:

extend google.protobuf.FileOptions {
  optional int32 source_retention_option = 1234 [retention = RETENTION_SOURCE];
}

注意:Protocol Buffers 22.0 起支持该特性,目前 C++/Java 已实现,Go 1.29.0+ 支持,Python 支持尚未发布。

选项目标(Option Targets)

字段的 targets 选项控制其可应用的实体类型(如仅允许应用于消息),protoc 会检查合法性。示例:

message MyOptions {
  // 仅允许应用于文件的选项
  string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  // 允许应用于消息和枚举的选项
  int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE, targets = TARGET_TYPE_ENUM];
}

生成代码

使用 Protocol Buffers 编译器(protoc)处理 .proto 文件,生成目标语言的代码。以下是详细步骤:

1. 安装依赖

  • 下载 protoc 编译器(参考官方文档)。
  • Go 语言需额外安装代码生成插件:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

2. 编译命令

protoc --proto_path=IMPORT_PATH \
  --cpp_out=DST_DIR \
  --java_out=DST_DIR \
  --python_out=DST_DIR \
  --go_out=DST_DIR \
  --ruby_out=DST_DIR \
  --objc_out=DST_DIR \
  --csharp_out=DST_DIR \
  path/to/file.proto

命令参数说明

  • --proto_path=IMPORT_PATH(简写 -I):指定导入文件的搜索目录(默认当前目录),可多次指定。
    • 注意:相对于 proto_path 的文件路径必须全局唯一(避免导入冲突)。
    • 推荐:将 proto_path 设置为项目顶层目录,确保导入路径全局唯一。
  • --xxx_out=DST_DIR:指定目标语言的代码输出目录,支持 .zip.jar 归档文件(会覆盖现有文件)。
  • 输入文件:一个或多个 .proto 文件(路径相对于当前目录,且必须位于 proto_path 下)。

3. 文件位置最佳实践

  • 不要将 .proto 文件与其他语言源代码放在同一目录,建议在项目根目录下创建 proto 子目录存放。
  • 路径应语言无关:避免将 .proto 文件放在 Java 源文件目录(非 Java 代码使用时路径无意义),推荐放在 //myteam/mypackage 等语言无关目录。
  • 例外:仅用于 Java 测试的 .proto 文件可放在 Java 源文件目录。

支持的平台

  • C++ 支持:参考《基础 C++ 支持政策》(Foundational C++ Support Policy)。
  • PHP 支持:参考《支持的 PHP 版本》(Supported PHP versions)。
  • 其他语言支持:参考对应语言的官方文档。
相关文章
|
18天前
|
人工智能 JSON 前端开发
|
9天前
|
存储 缓存 监控
pandas 3.0 内存调试指南:学会区分真假内存泄漏
本文揭秘pandas“内存不释放”的常见误解:非泄漏,实为CoW共享、Arrow缓冲池、视图隐式引用及分配器延迟归还OS内存所致。RSS≠真实占用,排查需结合tracemalloc、objgraph与原生指标,核心是管控引用生命周期。
140 12
pandas 3.0 内存调试指南:学会区分真假内存泄漏
|
Linux 网络安全 Android开发
振南技术干货集:各大平台串口调试软件大赏(1)
振南技术干货集:各大平台串口调试软件大赏(1)
|
9天前
|
人工智能 前端开发 开发者
这几个开源项目太火啦,抓紧收藏哟起来!!!
KnowNote:本地优先AI知识库桌面应用,无需Docker,私有化部署;Remotion:用React编程生成MP4视频的开源框架;Superpowers:为AI编程助手赋能的Agentic技能框架。三者均开源,各具创新特色!
136 11
|
12天前
|
人工智能 PyTorch 算法框架/工具
在无网络与8G内存约束下,个人计算机部署AI实践
本文探讨在无网络、仅8GB内存、无GPU的老旧PC上本地部署大语言模型的实践路径。通过构建纯净Python环境、选用PyTorch格式小模型(Qwen1.5-0.5B)、规避系统陷阱(如长路径限制、注册表关联错误等),成功实现离线中文对话,形成可复现的避坑指南与技术参考。(239字)
|
关系型数据库 MySQL Nacos
nacos数据库使用PostgreSQL及集群配置
从Nacos2.2版本开始,Nacos提供了数据源扩展插件,以便让需要进行其他数据库适配的用户自己编写插件来保存数据。
|
4月前
|
人工智能 监控 Java
构建定时 Agent,基于 Spring AI Alibaba 实现自主运行的人机协同智能 Agent
借助 Spring AI Alibaba 框架,开发者可快速实现定制化自动定时运行的 Agent,构建数据采集、智能分析到人工参与决策的全流程AI业务应用。
1538 65
|
2月前
|
人工智能 运维 安全
技术深析快手直播安全事件:为什么大量违规直播“关不掉”?
快手直播安全事件暴露了高并发下账号权限、风控与审核系统的系统性失效。对测试开发而言,需从功能验证转向系统性防控,强化极端场景测试、高负载审核链路验证及熔断机制演练,提升对复杂风险的预判与拦截能力。
|
搜索推荐 前端开发 架构师
阿里高级技术专家谈开源DDD框架:COLA4.0,分离架构和组件(上)
阿里高级技术专家谈开源DDD框架:COLA4.0,分离架构和组件(上)
3025 0
阿里高级技术专家谈开源DDD框架:COLA4.0,分离架构和组件(上)
|
7天前
|
人工智能 JSON Linux
玩转Ollama函数调用:让AI从“光说不练”到“动手解决问题”
你是否厌倦AI瞎编答案?Ollama函数调用功能为AI装上“小手”,让它能调用天气查询、计算器等自定义工具,先做事、再回答,告别胡说八道!本文手把手教你从零实现单次调用、并行调用、多轮智能体循环及流式响应,全程Python实战,小白也能轻松上手。