语言指南 (proto 3)
本指南描述了如何使用 protocol buffer 语言来构建您的 protocol buffer 数据,包括 .proto
文件语法以及如何从您的 .proto
文件生成数据访问类。它涵盖了 Protocol Buffers 语言的 proto3 版本。
有关 版本 (editions) 语法的更多信息,请参阅Protobuf 版本语言指南。
有关 proto2 语法的信息,请参阅Proto2 语言指南。
这是一份参考指南 – 有关使用本文档中描述的许多功能的逐步示例,请参阅您所选语言的教程。
定义消息类型
首先来看一个非常简单的示例。假设您想定义一个搜索请求消息格式,其中每个搜索请求包含一个查询字符串、您感兴趣的结果的特定页面以及每页的结果数量。这是您用来定义该消息类型的 .proto
文件。
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
文件的第一行指定您使用的是 protobuf 语言规范的 proto3 版本。
edition
(或 proto2/proto3 的syntax
)必须是文件的第一个非空、非注释行。- 如果未指定
edition
或syntax
,protocol buffer 编译器将假定您使用的是proto2。
SearchRequest
消息定义指定了三个字段(名称/值对),每个字段对应您想要包含在该消息类型中的一部分数据。每个字段都有一个名称和一个类型。
指定字段类型
在前面的示例中,所有字段都是标量类型:两个整数(page_number
和 results_per_page
)和一个字符串(query
)。您还可以为您的字段指定枚举以及其他消息类型等复合类型。
分配字段编号
您必须为您消息定义中的每个字段指定一个介于 1
和 536,870,911
之间的编号,并遵循以下限制
- 给定编号在该消息的所有字段中必须唯一。
- 字段编号
19,000
到19,999
保留用于 Protocol Buffers 实现。如果您在消息中使用这些保留字段编号中的一个,protocol buffer 编译器将发出警告。 - 您不能使用任何以前保留的字段编号,也不能使用分配给扩展的任何字段编号。
一旦您的消息类型投入使用,该编号不能更改,因为它在消息线格式中标识了该字段。 “更改”字段编号相当于删除该字段并创建一个具有相同类型但新编号的新字段。有关如何正确执行此操作,请参阅删除字段。
字段编号绝不应该被重用。切勿将字段编号从保留列表中取出以与新字段定义重用。请参阅重用字段编号的后果。
对于最常设置的字段,您应该使用 1 到 15 的字段编号。较低的字段编号在线格式中占用更少的空间。例如,字段编号在 1 到 15 的范围内编码只需要一个字节。字段编号在 16 到 2047 的范围内需要两个字节。您可以在Protocol Buffer 编码中找到更多相关信息。
重用字段编号的后果
重用字段编号会使线格式消息的解码变得模糊不清。
protobuf 线格式精简,不提供检测使用一种定义编码、使用另一种定义解码的字段的方法。
使用一种定义编码字段,然后使用不同定义解码同一字段可能会导致
- 开发者浪费调试时间
- 解析/合并错误(最好的情况)
- PII/SPII 泄露
- 数据损坏
字段编号重用的常见原因
- 重新编号字段(有时为了使字段编号顺序更美观)。重新编号实际上删除了所有涉及重新编号的字段并重新添加它们,导致不兼容的线格式更改。
- 删除字段但未保留该编号以防止将来重用。
字段编号限制为 29 位而不是 32 位,因为使用了三个位来指定字段的线格式。有关更多信息,请参阅编码主题。
指定字段基数
消息字段可以是以下之一
单一 (Singular):
在 proto3 中,有两种类型的单一字段
optional
:(推荐)一个optional
字段有两种可能的状态- 字段已设置,并包含一个被明确设置或从线格式解析的值。它将被序列化到线格式中。
- 字段未设置,将返回默认值。它不会被序列化到线格式中。
您可以检查该值是否被明确设置。
为了最大限度地与 protobuf 版本和 proto2 兼容,推荐使用
optional
而非隐式字段。隐式:(不推荐)隐式字段没有明确的基数标签,并且行为如下
如果字段是消息类型,它的行为就像一个
optional
字段。如果字段不是消息类型,它有两种状态
- 字段设置为非默认值(非零),该值被明确设置或从线格式解析。它将被序列化到线格式中。
- 字段设置为默认值(零)。它不会被序列化到线格式中。事实上,您无法确定默认值(零)是已被设置或从线格式解析,还是根本未提供。有关此主题的更多信息,请参阅字段存在性。
repeated
:此字段类型在格式良好的消息中可以重复零次或多次。重复值的顺序将得到保留。map
:这是一种成对的键/值字段类型。有关此字段类型的更多信息,请参阅映射 (Maps)。
重复字段默认打包
在 proto3 中,标量数值类型的 repeated
字段默认使用 packed
编码。
您可以在Protocol Buffer 编码中找到有关 packed
编码的更多信息。
消息类型字段总是具有字段存在性
在 proto3 中,消息类型字段已经具有字段存在性。因此,添加 optional
修饰符不会改变该字段的字段存在性。
以下代码示例中 Message2
和 Message3
的定义会为所有语言生成相同的代码,并且在二进制、JSON 和 TextFormat 中的表示没有差异
syntax="proto3";
package foo.bar;
message Message1 {}
message Message2 {
Message1 foo = 1;
}
message Message3 {
optional Message1 bar = 1;
}
格式良好的消息
术语“格式良好 (well-formed)”应用于 protobuf 消息时,指的是序列化/反序列化后的字节。 protoc 解析器会验证给定的 proto 定义文件是否可解析。
单一字段可以在线格式字节中出现多次。解析器将接受输入,但只有该字段的最后一个实例可以通过生成的绑定进行访问。有关此主题的更多信息,请参阅最后一个优先 (Last One Wins)。
添加更多消息类型
可以在单个 .proto
文件中定义多个消息类型。如果您要定义多个相关消息,这会很有用 – 因此,例如,如果您想定义与 SearchResponse
消息类型对应的回复消息格式,您可以将其添加到同一个 .proto
文件中
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
合并消息导致膨胀 虽然可以在单个 .proto
文件中定义多种消息类型(例如 message、enum 和 service),但当大量具有不同依赖关系的消息定义在同一个文件中时,这也会导致依赖膨胀。建议每个 .proto
文件包含尽可能少的消息类型。
添加注释
要在您的 .proto
文件中添加注释
优先使用 C/C++/Java 行尾式注释 ‘//’,将其放在 .proto 代码元素的前一行
也接受 C 风格的内联/多行注释
/* ... */
。- 使用多行注释时,优先使用以 ‘*’ 开头的边缘行。
/**
* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response.
*/
message SearchRequest {
string query = 1;
// Which page number do we want?
int32 page_number = 2;
// Number of results to return per page.
int32 results_per_page = 3;
}
删除字段
如果处理不当,删除字段可能会导致严重问题。
当您不再需要某个字段并且已从客户端代码中删除所有引用时,您可以从消息中删除该字段定义。但是,您必须保留已删除的字段编号。如果您不保留字段编号,将来开发者可能会重用该编号。
您还应该保留字段名称,以便您的消息的 JSON 和 TextFormat 编码能够继续解析。
保留字段编号
如果您通过完全删除或注释掉某个字段来更新消息类型,将来的开发者在对该类型进行自己的更新时可能会重用该字段编号。这可能导致严重问题,如重用字段编号的后果中所述。为确保这种情况不会发生,请将您删除的字段编号添加到 reserved
列表中。
如果将来有任何开发者试图使用这些保留字段编号,protoc 编译器将生成错误消息。
message Foo {
reserved 2, 15, 9 to 11;
}
保留字段编号范围是包含结束值的(9 到 11
与 9, 10, 11
相同)。
保留字段名称
稍后重用旧字段名称通常是安全的,但在使用 TextProto 或 JSON 编码时,字段名称会被序列化,此时重用可能不安全。为避免此风险,您可以将已删除的字段名称添加到 reserved
列表中。
保留名称仅影响 protoc 编译器的行为,而不影响运行时行为,一个例外是:TextProto 实现可能会在解析时丢弃具有保留名称的未知字段(不会像处理其他未知字段那样引发错误)(目前只有 C++ 和 Go 实现是这样做的)。运行时 JSON 解析不受保留名称的影响。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
请注意,您不能在同一个 reserved
语句中混合使用字段名称和字段编号。
从您的 .proto
文件生成了什么?
当您在 .proto
文件上运行protocol buffer 编译器时,编译器会用您选择的语言生成处理文件中描述的消息类型所需的代码,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
- 对于 C++,编译器会从每个
.proto
文件生成一个.h
和.cc
文件,文件中描述的每种消息类型都有一个对应的类。 - 对于 Java,编译器会生成一个
.java
文件,其中包含每种消息类型的一个类,以及一个用于创建消息类实例的特殊Builder
类。 - 对于 Kotlin,除了 Java 生成的代码外,编译器还会为每种消息类型生成一个
.kt
文件,其中包含改进的 Kotlin API。这包括一个简化创建消息实例的 DSL,一个可空字段访问器,以及一个复制函数。 - Python 有点不同——Python 编译器会生成一个模块,其中包含您
.proto
文件中每种消息类型的静态描述符,然后将其与一个元类 (metaclass) 一起使用,在运行时创建必要的 Python 数据访问类。 - 对于 Go,编译器会生成一个
.pb.go
文件,其中包含您文件中每种消息类型的一个类型。 - 对于 Ruby,编译器会生成一个
.rb
文件,其中包含一个包含您的消息类型的 Ruby 模块。 - 对于 Objective-C,编译器会从每个
.proto
文件生成一个pbobjc.h
和pbobjc.m
文件,文件中描述的每种消息类型都有一个对应的类。 - 对于 C#,编译器会从每个
.proto
文件生成一个.cs
文件,文件中描述的每种消息类型都有一个对应的类。 - 对于 PHP,编译器会为您文件中描述的每种消息类型生成一个
.php
消息文件,并为您编译的每个.proto
文件生成一个.php
元数据文件。元数据文件用于将有效的消息类型加载到描述符池中。 - 对于 Dart,编译器会生成一个
.pb.dart
文件,其中包含您文件中每种消息类型的一个类。
您可以通过遵循您所选语言的教程来了解更多关于使用每种语言的 API 的信息。有关更多 API 详细信息,请参阅相关的API 参考。
标量值类型
标量消息字段可以具有以下类型之一——表格显示了在 .proto
文件中指定的类型,以及自动生成的类中对应的类型
Proto 类型 | 备注 |
---|---|
double | |
float | |
int32 | 使用变长编码。对负数编码效率低下——如果您的字段可能包含负值,请改用 sint32。 |
int64 | 使用变长编码。对负数编码效率低下——如果您的字段可能包含负值,请改用 sint64。 |
uint32 | 使用变长编码。 |
uint64 | 使用变长编码。 |
sint32 | 使用变长编码。有符号整数值。与常规 int32 相比,它们对负数编码更高效。 |
sint64 | 使用变长编码。有符号整数值。与常规 int64 相比,它们对负数编码更高效。 |
fixed32 | 总是四个字节。如果值经常大于 228,比 uint32 更高效。 |
fixed64 | 总是八个字节。如果值经常大于 256,比 uint64 更高效。 |
sfixed32 | 总是四个字节。 |
sfixed64 | 总是八个字节。 |
bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,且长度不能超过 232。 |
bytes | 可以包含任何不超过 232 字节的任意字节序列。 |
Proto 类型 | C++ 类型 | Java/Kotlin 类型[1] | Python 类型[3] | 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[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
uint32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (根据需要) | uint | integer | int | u32 |
uint64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sint32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根据需要) | int | integer | int | i32 |
sint64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
fixed32 | uint32_t | int[2] | int/long[4] | uint32 | Fixnum 或 Bignum (根据需要) | uint | integer | int | u32 |
fixed64 | uint64_t | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | u64 |
sfixed32 | int32_t | int | int | int32 | Fixnum 或 Bignum (根据需要) | int | integer | int | i32 |
sfixed64 | int64_t | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | i64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | bool |
string | std::string | String | str/unicode[5] | 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,则也可以是 int。在所有情况下,设置的值必须适合所表示的类型。参见 [2]。
[5] Python 字符串在解码时表示为 unicode,但如果给定 ASCII 字符串则可以是 str(这可能会发生变化)。
[6] 在 64 位机器上使用 Integer,在 32 位机器上使用 string。
您可以在Protocol Buffer 编码中找到有关序列化消息时这些类型的编码方式的更多信息。
默认字段值
解析消息时,如果编码后的消息字节不包含特定字段,则访问解析对象中的该字段会返回该字段的默认值。默认值是类型特定的
- 对于字符串,默认值为空字符串。
- 对于 bytes,默认值为空 bytes。
- 对于 bools,默认值为 false。
- 对于数值类型,默认值为零。
- 对于消息字段,字段未设置。其确切值取决于语言。有关详细信息,请参阅生成的代码指南。
- 对于枚举,默认值是定义的第一个枚举值,该值必须为 0。请参阅枚举默认值。
重复字段的默认值为空(通常是相应语言中的空列表)。
map 字段的默认值为空(通常是相应语言中的空 map)。
请注意,对于隐式存在性的标量字段,消息解析后无法判断该字段是明确设置为默认值(例如布尔值设置为 false
)还是根本未设置:您在定义消息类型时应记住这一点。例如,如果某个布尔值设置为 false
时会启用某种行为,而您不希望该行为也默认发生,则不要设置该布尔值。另请注意,如果标量消息字段确实设置为默认值,该值将不会在线格式中序列化。如果 float 或 double 值设置为 +0,它将不会被序列化,但 -0 被认为是不同的,将被序列化。
有关您所选语言中默认值如何在生成代码中工作的更多详细信息,请参阅生成的代码指南。
枚举
当您定义消息类型时,您可能希望其某个字段仅具有预定义值列表中的一个。例如,假设您想为每个 SearchRequest
添加一个 corpus
字段,其中 corpus 可以是 UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
、PRODUCTS
或 VIDEO
。您只需在消息定义中添加一个 enum
,并为每个可能的值添加一个常量,就可以非常简单地实现这一点。
在下面的示例中,我们添加了一个名为 Corpus
的 enum
,其中包含所有可能的值,以及一个类型为 Corpus
的字段
enum Corpus {
CORPUS_UNSPECIFIED = 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;
}
枚举默认值
SearchRequest.corpus
字段的默认值是 CORPUS_UNSPECIFIED
,因为这是枚举中定义的第一个值。
在 proto3 中,枚举定义中定义的第一个值必须为零,并且名称应为 ENUM_TYPE_NAME_UNSPECIFIED
或 ENUM_TYPE_NAME_UNKNOWN
。这是因为
还建议第一个默认值除了表示“此值未指定”之外没有其他语义含义。
枚举值别名
您可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将 allow_alias
选项设置为 true
。否则,protocol buffer 编译器在发现别名时会生成警告消息。尽管所有别名值在序列化时都有效,但反序列化时只使用第一个值。
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}
enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // Uncommenting this line will cause a warning message.
ENAA_FINISHED = 2;
}
枚举器常量必须在 32 位整数的范围内。由于 enum
值在线格式中使用变长编码,负值效率低下,因此不建议使用。您可以在消息定义中定义 enum
,如前面的示例所示,或者在消息定义之外定义——这些 enum
可以在您的 .proto
文件中的任何消息定义中重用。您还可以使用在一个消息中声明的 enum
类型作为另一个消息中字段的类型,使用语法 _MessageType_._EnumType_
。
当您在使用了 enum
的 .proto
文件上运行 protocol buffer 编译器时,生成的代码将为 Java、Kotlin 或 C++ 提供相应的 enum
,或为 Python 提供特殊的 EnumDescriptor
类,该类用于在运行时生成的类中创建一组具有整数值的符号常量。
重要提示
生成的代码可能受限于特定语言对枚举器数量的限制(对于某些语言来说,数量可能只有几千个)。请查看您计划使用的语言的限制。反序列化期间,无法识别的枚举值将被保留在消息中,尽管这在消息反序列化时的表示方式取决于语言。在支持具有超出指定符号范围的值的开放枚举类型的语言中,例如 C++ 和 Go,未知枚举值仅以其底层整数表示形式存储。在具有封闭枚举类型的语言中,例如 Java,枚举中的一个 case 用于表示无法识别的值,并且可以通过特殊访问器访问底层整数。在任何一种情况下,如果消息被序列化,无法识别的值仍将随消息一起序列化。
重要提示
有关枚举应该如何工作以及它们目前在不同语言中的工作方式的对比信息,请参阅枚举行为。有关如何在应用程序中使用消息 enum
的更多信息,请参阅您所选语言的生成的代码指南。
保留值
如果您通过完全移除或注释掉一个枚举条目来更新枚举类型,将来的用户在对该类型进行自己的更新时可能会重用该数值。如果他们稍后加载相同的 .proto
的旧实例,这可能会导致严重问题,包括数据损坏、隐私错误等。确保这种情况不会发生的一种方法是指定您删除的条目的数值(和/或名称,这也可能导致 JSON 序列化问题)是 reserved
。如果将来有任何用户试图使用这些标识符,protocol buffer 编译器将发出警告。您可以使用 max
关键字指定保留数值范围达到最大可能值。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
请注意,您不能在同一个 reserved
语句中混合使用字段名称和数值。
使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设您想在每个 SearchResponse
消息中包含 Result
消息——为此,您可以在同一个 .proto
文件中定义一个 Result
消息类型,然后在 SearchResponse
中指定一个类型为 Result
的字段
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入定义
在前面的示例中,Result
消息类型与 SearchResponse
定义在同一个文件中——如果您想用作字段类型的消息类型已经定义在另一个 .proto
文件中怎么办?
您可以通过导入 (importing) 其他 .proto
文件的定义来使用它们。要导入另一个 .proto
文件的定义,请在文件顶部添加一个导入语句
import "myproject/other_protos.proto";
默认情况下,您只能使用直接导入的 .proto
文件中的定义。但是,有时您可能需要将 .proto
文件移动到新位置。与其直接移动 .proto
文件并在一次更改中更新所有调用点,您可以在旧位置放置一个占位符 .proto
文件,使用 import public
概念将所有导入转发到新位置。
注意: Java 中可用的公共导入功能在移动整个 .proto 文件或使用 java_multiple_files = true
时最有效。在这些情况下,生成的名称保持稳定,无需更新代码中的引用。虽然在未设置 java_multiple_files = true
的情况下移动 .proto 文件的一部分时技术上可行,但这样做需要同时更新许多引用,因此可能不会显著简化迁移。该功能在 Kotlin、TypeScript、JavaScript、GCL 或使用 protobuf 静态反射的 C++ 目标中不可用。
包含 import public
语句的 proto 文件所导入的任何代码都可以传递地依赖于 import public
依赖项。例如
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
协议编译器会在使用 -I
/--proto_path
标志指定的目录集中搜索导入的文件。如果未给定标志,它将在调用编译器的目录中查找。通常,您应该将 --proto_path
标志设置为项目根目录,并对所有导入使用完全限定名称。
使用 proto2 消息类型
可以导入proto2 消息类型并在您的 proto3 消息中使用它们,反之亦然。但是,proto2 枚举不能直接在 proto3 语法中使用(如果导入的 proto2 消息使用它们则可以)。
嵌套类型
您可以在其他消息类型中定义和使用消息类型,如以下示例所示——这里 Result
消息定义在 SearchResponse
消息内部
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父消息类型之外重用此消息类型,请将其引用为 _Parent_._Type_
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
您可以随意嵌套消息。在下面的示例中,请注意命名为 Inner
的两个嵌套类型完全独立,因为它们定义在不同的消息中
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
更新消息类型
如果现有的消息类型不再满足您的所有需求——例如,您希望消息格式有一个额外的字段——但您仍然想使用使用旧格式创建的代码,请不要担心!使用二进制线格式时,更新消息类型而不会破坏任何现有代码非常简单。
注意
如果您使用 JSON 或proto 文本格式存储 protocol buffer 消息,您在 proto 定义中可以进行的更改是不同的。查看Proto 最佳实践和以下规则
- 不要更改任何现有字段的字段编号。“更改”字段编号相当于删除该字段并添加一个具有相同类型的新字段。如果要重新编号字段,请参阅删除字段的说明。
- 如果您添加新字段,使用“旧”消息格式的代码序列化的任何消息仍然可以通过新的生成代码解析。您应该记住这些元素的默认值,以便新代码可以与旧代码生成的消息正确交互。同样,您的新代码创建的消息可以被旧代码解析:旧的二进制文件在解析时会简单地忽略新字段。详细信息请参阅未知字段部分。
- 字段可以被删除,只要在您的更新消息类型中不再使用该字段编号即可。您可能希望重命名该字段,也许添加前缀“OBSOLETE_”,或者将字段编号保留,以防止您的
.proto
的未来用户意外重用该编号。 int32
、uint32
、int64
、uint64
和bool
都是兼容的——这意味着您可以将字段从这些类型中的一种更改为另一种,而不会破坏向前或向后兼容性。如果从线格式解析的数字不符合相应的类型,您将获得与在 C++ 中将该数字强制转换为该类型相同的效果(例如,如果将 64 位数字读取为 int32,它将被截断为 32 位)。sint32
和sint64
彼此兼容,但与其余整数类型不兼容。如果写入的值在 INT_MIN 和 INT_MAX(包含边界)之间,则使用任一类型解析时其值将保持相同。如果写入的 sint64 值超出该范围并解析为 sint32,则变长整数将被截断为 32 位,然后进行 zigzag 解码(这将导致观察到不同的值)。string
和bytes
兼容,只要 bytes 是有效的 UTF-8。- 嵌入消息与
bytes
兼容,如果 bytes 包含消息的编码实例。 fixed32
与sfixed32
兼容,fixed64
与sfixed64
兼容。- 对于
string
、bytes
和消息字段,singular 与repeated
兼容。给定重复字段的序列化数据作为输入,期望此字段为 singular 的客户端如果是原始类型字段,将取最后一个输入值;如果是消息类型字段,将合并所有输入元素。请注意,这对于数值类型(包括 bool 和 enum)通常是不安全的。数值类型的重复字段默认以 packed 格式序列化,当期望 singular 字段时,将无法正确解析。 enum
在线格式方面与int32
、uint32
、int64
和uint64
兼容(注意,如果值不适合,将被截断)。但是,请注意,客户端代码在消息反序列化时可能会以不同的方式处理它们:例如,无法识别的 proto3enum
值将被保留在消息中,但这在消息反序列化时的表示方式取决于语言。 Int 字段总是只保留其值。- 将单个
optional
字段或扩展变更为新oneof
的成员是二进制兼容的,但对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。因此,Google 在其公共 API 中不会进行此类更改,如 AIP-180 中所述。在具有相同源代码兼容性警告的前提下,将多个字段移动到新的oneof
中可能是安全的,如果您确定没有代码同时设置了多个字段。将字段移动到现有的oneof
中是不安全的。同样,将单个字段oneof
更改为optional
字段或扩展是安全的。 - 将字段从
map<K, V>
更改为相应的repeated
消息字段是二进制兼容的(有关消息布局和其他限制,请参阅下面的映射 (Maps))。但是,更改的安全性取决于应用程序:当反序列化和重新序列化消息时,使用repeated
字段定义的客户端将产生语义上相同的结果;但是,使用map
字段定义的客户端可能会重新排序条目并丢弃具有重复键的条目。
未知字段
未知字段是格式良好的 protocol buffer 序列化数据,表示解析器无法识别的字段。例如,当旧的二进制文件解析由包含新字段的新二进制文件发送的数据时,这些新字段在旧的二进制文件中就变成了未知字段。
Proto3 消息保留未知字段,并在解析和序列化输出中包含它们,这与 proto2 的行为一致。
保留未知字段
某些操作可能导致未知字段丢失。例如,如果您执行以下任一操作,未知字段会丢失
- 将 proto 序列化为 JSON。
- 遍历消息中的所有字段以填充新消息。
为了避免丢失未知字段,请执行以下操作
- 使用二进制格式;避免使用文本格式进行数据交换。
- 使用面向消息的 API,例如
CopyFrom()
和MergeFrom()
,以复制数据,而不是逐字段复制
TextFormat 有点特殊。序列化到 TextFormat 会使用字段编号打印未知字段。但是,如果存在使用字段编号的条目,将 TextFormat 数据解析回二进制 proto 将失败。
Any 类型
Any
消息类型允许您将消息用作嵌入类型,而无需其 .proto 定义。一个 Any
包含一个任意序列化的消息作为 bytes
,以及一个充当该消息类型全局唯一标识符并解析为该消息类型的 URL。要使用 Any
类型,您需要导入 google/protobuf/any.proto
。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认类型 URL 为 type.googleapis.com/_packagename_._messagename_
。
不同的语言实现将支持运行时库助手,以类型安全的方式打包和解包 Any
值——例如,在 Java 中,Any
类型将具有特殊的 pack()
和 unpack()
访问器,而在 C++ 中则有 PackFrom()
和 UnpackTo()
方法
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
Oneof
如果您的消息包含许多单一字段,并且最多只有一个字段会同时被设置,您可以使用 oneof 特性来强制执行此行为并节省内存。
Oneof 字段类似于 optional 字段,但 oneof 中的所有字段共享内存,并且最多只能同时设置一个字段。设置 oneof 的任何成员都会自动清除所有其他成员。您可以使用特殊的 case()
或 WhichOneof()
方法(取决于您选择的语言)来检查 oneof 中哪个值已设置(如果有)。
请注意,如果设置了多个值,则由 proto 文件中的顺序确定的最后一个设置值将覆盖所有先前的值。
oneof 字段的字段编号在 enclosing 消息中必须唯一。
使用 Oneof
要在您的 .proto
中定义 oneof,您使用 oneof
关键字,后跟您的 oneof 名称,在此示例中为 test_oneof
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后将 oneof 字段添加到 oneof 定义中。您可以添加除 map
字段和 repeated
字段之外的任何类型的字段。如果需要将 repeated 字段添加到 oneof,可以使用包含 repeated 字段的消息。
在生成的代码中,oneof 字段与常规字段具有相同的 getter 和 setter。您还会获得一个特殊方法,用于检查 oneof 中哪个值(如果有)已设置。您可以在相关的API 参考中找到有关您所选语言的 oneof API 的更多信息。
Oneof 特性
设置 oneof 字段会自动清除 oneof 的所有其他成员。因此,如果您设置了多个 oneof 字段,只有您设置的最后一个字段仍然具有值。
SampleMessage message; message.set_name("name"); CHECK_EQ(message.name(), "name"); // Calling mutable_sub_message() will clear the name field and will set // sub_message to a new instance of SubMessage with none of its fields set. message.mutable_sub_message(); CHECK(message.name().empty());
如果解析器在线格式中遇到同一个 oneof 的多个成员,则在解析消息中只使用遇到的最后一个成员。在线格式中解析数据时,从字节开头开始,评估下一个值,并应用以下解析规则
首先,检查同一个 oneof 中是否有不同字段当前已设置,如果是则清除它。
然后应用内容,就像该字段不在 oneof 中一样
- 原始类型将覆盖任何已设置的值
- 消息类型将合并到任何已设置的值中
Oneof 不能是
repeated
。反射 API 适用于 oneof 字段。
如果将 oneof 字段设置为默认值(例如将 int32 oneof 字段设置为 0),则该 oneof 字段的“case”将被设置,并且该值将在线格式中序列化。
如果您正在使用 C++,请确保您的代码不会导致内存崩溃。下面的示例代码将崩溃,因为调用
set_name()
方法已经删除了sub_message
。SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes here
同样在 C++ 中,如果您对两个包含 oneof 的消息进行
Swap()
,则每个消息最终将拥有对方的 oneof case:在下面的示例中,msg1
将具有sub_message
,而msg2
将具有name
。SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK_EQ(msg2.name(), "name");
向后兼容性问题
添加或移除 oneof 字段时要小心。如果检查 oneof 的值返回 None
/NOT_SET
,这可能意味着 oneof 尚未设置,或者它已设置为不同版本 oneof 中的字段。无法区分,因为无法知道线格式上的未知字段是否是 oneof 的成员。
标签重用问题
- 将单一字段移动到或移出 oneof:消息序列化和解析后,您可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将单个字段移动到新 oneof 中,如果已知只有一个字段被设置,您也可能可以移动多个字段。有关更多详细信息,请参阅更新消息类型。
- 删除 oneof 字段并重新添加:这可能会在消息序列化和解析后清除您当前设置的 oneof 字段。
- 拆分或合并 oneof:这与移动单一字段有类似的问题。
映射 (Maps)
如果想创建数据定义中的关联映射,protocol buffers 提供了一个方便的快捷语法
map<key_type, value_type> map_field = N;
…其中 key_type
可以是任何整数或字符串类型(即,除了浮点类型和 bytes
之外的任何标量类型)。请注意,enum 和 proto 消息都不适用于 key_type
。value_type
可以是除另一个 map 之外的任何类型。
因此,例如,如果您想创建一个项目 map,其中每个 Project
消息都关联一个字符串键,您可以这样定义它
map<string, Project> projects = 3;
映射 (Maps) 特性
- Map 字段不能是
repeated
。 - Map 值的线格式顺序和迭代顺序是未定义的,因此您不能依赖您的 map 项具有特定的顺序。
- 为
.proto
生成文本格式时,map 按键排序。数值键按数值排序。 - 从线格式解析或合并时,如果存在重复的 map 键,则使用遇到的最后一个键。从文本格式解析 map 时,如果存在重复键,解析可能会失败。
- 如果您为 map 字段提供了键但未提供值,则字段序列化时的行为取决于语言。在 C++、Java、Kotlin 和 Python 中,序列化该类型的默认值,而在其他语言中则不序列化任何内容。
- 在 map
foo
的同一作用域内不能存在符号FooEntry
,因为FooEntry
已被 map 的实现使用。
生成的 map API 目前支持所有受支持的语言。您可以在相关的API 参考中找到有关您所选语言的 map API 的更多信息。
向后兼容性
map 语法在在线格式中等同于以下内容,因此不支持 map 的 protocol buffers 实现仍然可以处理您的数据
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
任何支持 map 的 protocol buffers 实现都必须生成和接受可以被前述定义接受的数据。
包 (Packages)
您可以在 .proto
文件中添加可选的 package
说明符,以防止 protocol message 类型之间发生名称冲突。
package foo.bar;
message Open { ... }
然后,在定义消息类型的字段时,可以使用 package 说明符
message Foo {
...
foo.bar.Open open = 1;
...
}
package 说明符影响生成代码的方式取决于您选择的语言
- 在 C++ 中,生成的类被包装在 C++ 命名空间中。例如,
Open
将位于foo::bar
命名空间中。 - 在 Java 和 Kotlin 中,除非您在
.proto
文件中明确提供了option java_package
,否则 proto 包(使用.proto
文件中的“package”关键字指定)将用作 Java 包。 - 在 Python 中,
package
指令被忽略,因为 Python 模块是根据其在文件系统中的位置组织的。 - 在 Go 中,
package
指令被忽略,生成的.pb.go
文件位于与其对应的go_proto_library
Bazel 规则同名的包中。对于开源项目,您必须提供go_package
选项或设置 Bazel 的-M
标志。 - 在 Ruby 中,生成的类被包装在嵌套的 Ruby 命名空间中,并转换为所需的 Ruby 大写风格(首字母大写;如果首字母不是字母,则添加前缀
PB_
)。例如,Open
将位于Foo::Bar
命名空间中。 - 在 PHP 中,在转换为 PascalCase 后,该包被用作命名空间,除非您在
.proto
文件中明确提供了option php_namespace
。例如,Open
将位于Foo\Bar
命名空间中。 - 在 C# 中,在转换为 PascalCase 后,该包被用作命名空间,除非您在
.proto
文件中明确提供了option csharp_namespace
。例如,Open
将位于Foo.Bar
命名空间中。
请注意,即使 package
指令不直接影响生成的代码(例如在 Python 中),仍然强烈建议为 .proto
文件指定包,因为否则可能导致描述符中出现命名冲突,并使 proto 对于其他语言不可移植。
包 (Packages) 和名称解析
protocol buffer 语言中的类型名称解析类似于 C++:首先搜索最内层的作用域,然后是次内层的作用域,依此类推,每个包都被视为其父包的“内层”。开头的 ‘.’(例如,.foo.bar.Baz
)表示从最外层的作用域开始查找。
protocol buffer 编译器通过解析导入的 .proto
文件来解析所有类型名称。每种语言的代码生成器都知道如何在那种语言中引用每种类型,即使它具有不同的作用域规则。
定义服务
如果您希望在 RPC (Remote Procedure Call) 系统中使用您的消息类型,您可以在 .proto
文件中定义一个 RPC 服务接口,protocol buffer 编译器将用您选择的语言生成服务接口代码和存根。因此,例如,如果您想定义一个 RPC 服务,其中包含一个接收您的 SearchRequest
并返回 SearchResponse
的方法,您可以在 .proto
文件中按如下方式定义
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
与 protocol buffer 一起使用的最直接的 RPC 系统是 gRPC:这是一个由 Google 开发的语言和平台中立的开源 RPC 系统。gRPC 与 protocol buffer 配合得非常好,并允许您使用特殊的 protocol buffer 编译器插件直接从您的 .proto
文件生成相关的 RPC 代码。
如果您不想使用 gRPC,也可以将 protocol buffer 与您自己的 RPC 实现一起使用。您可以在Proto2 语言指南中了解更多相关信息。
还有一些正在进行中的第三方项目正在开发 Protocol Buffers 的 RPC 实现。有关我们所知的项目链接列表,请参阅第三方附加组件 wiki 页面。
JSON 映射
标准的 protobuf 二进制线格式是使用 protobuf 的两个系统之间通信的首选序列化格式。对于与使用 JSON 而非 protobuf 线格式的系统进行通信,Protobuf 支持JSON 中的规范编码。
选项 (Options)
.proto
文件中的 individual declarations 可以使用许多选项 (options) 进行标注。选项不会改变 declaration 的总体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表定义在/google/protobuf/descriptor.proto
中。
一些选项是文件级选项,这意味着它们应该写在顶层作用域,而不是任何消息、枚举或服务定义内部。一些选项是消息级选项,这意味着它们应该写在消息定义内部。一些选项是字段级选项,这意味着它们应该写在字段定义内部。选项也可以写在枚举类型、枚举值、oneof 字段、服务类型和服务方法上;但是,目前对于这些都没有有用的选项。
以下是一些最常用的选项
java_package
(文件选项):您想要用于生成的 Java/Kotlin 类的包。如果在.proto
文件中没有明确给定java_package
选项,则默认使用 proto 包(使用.proto
文件中的“package”关键字指定)。但是,proto 包通常不是好的 Java 包,因为 proto 包预计不以反向域名开头。如果不生成 Java 或 Kotlin 代码,此选项无效。option java_package = "com.example.foo";
java_outer_classname
(文件选项):您想要生成的包装 Java 类的类名(以及文件名)。如果在.proto
文件中没有明确指定java_outer_classname
,则类名将通过将.proto
文件名转换为驼峰命名法来构建(因此foo_bar.proto
变为FooBar.java
)。如果禁用了java_multiple_files
选项,则为.proto
文件生成的其余所有类/枚举等将作为嵌套类/枚举等生成在此外部包装 Java 类内部。如果不生成 Java 代码,此选项无效。option java_outer_classname = "Ponycopter";
java_multiple_files
(文件选项):如果为 false,则此.proto
文件只生成一个.java
文件,并且为顶级消息、服务和枚举生成的 Java 类/枚举等都将嵌套在一个外部类中(参见java_outer_classname
)。如果为 true,则为顶级消息、服务和枚举生成的每个 Java 类/枚举等都将生成单独的.java
文件,并且为该.proto
文件生成的包装 Java 类将不包含任何嵌套类/枚举等。这是一个布尔选项,默认为false
。如果不生成 Java 代码,此选项无效。option java_multiple_files = true;
optimize_for
(文件选项):可以设置为SPEED
、CODE_SIZE
或LITE_RUNTIME
。这会影响 C++ 和 Java 代码生成器(以及可能的第三方生成器),方式如下SPEED
(默认):protocol buffer 编译器将生成用于序列化、解析和对您的消息类型执行其他常见操作的代码。此代码经过高度优化。CODE_SIZE
:protocol buffer 编译器将生成最小类,并依靠共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED
时小得多,但操作会更慢。类仍将实现与在SPEED
模式下完全相同的公共 API。此模式在包含大量.proto
文件且不需要所有文件都速度极快的应用程序中最有用。LITE_RUNTIME
:protocol buffer 编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite
而非libprotobuf
)的类。lite 运行时比完整库小得多(大约小一个数量级),但省略了描述符和反射等某些特性。这对于在移动电话等受限平台上运行的应用程序特别有用。编译器仍然会像在SPEED
模式下一样生成所有方法的快速实现。生成的类将仅实现每种语言中的MessageLite
接口,该接口仅提供完整Message
接口方法的子集。
option optimize_for = CODE_SIZE;
cc_generic_services
、java_generic_services
、py_generic_services
(文件选项):通用服务已弃用。 protocol buffer 编译器是否应该分别在 C++、Java 和 Python 中根据服务定义生成抽象服务代码。出于历史原因,这些选项默认为true
。但是,自 2.3.0 版(2010 年 1 月)起,对于 RPC 实现而言,提供代码生成器插件以生成更特定于每个系统的代码,而不是依赖于“抽象”服务,被认为是更可取的方式。// This file relies on plugins to generate service code. option cc_generic_services = false; option java_generic_services = false; option py_generic_services = false;
cc_enable_arenas
(文件选项):为 C++ 生成的代码启用arena allocation。objc_class_prefix
(文件选项):设置 Objective-C 类前缀,该前缀将添加到由此 .proto 生成的所有 Objective-C 类和枚举的前面。没有默认值。您应该使用 3-5 个大写字符的长度作为Apple 推荐的前缀。请注意,所有 2 个字母的前缀都被 Apple 保留。packed
(字段选项):对于基本数值类型的重复字段,默认值为true
,导致使用更紧凑的编码。要使用 unpacked wireformat,可以将其设置为false
。这提供了与 2.3.0 版之前解析器的兼容性(很少需要),如下例所示repeated int32 samples = 4 [packed = false];
deprecated
(字段选项):如果设置为true
,表示该字段已弃用,不应在新代码中使用。在大多数语言中,这实际上没有效果。在 Java 中,这会变成@Deprecated
注解。对于 C++,clang-tidy 会在使用弃用字段时生成警告。将来,其他特定语言的代码生成器可能会在字段的访问器上生成弃用注解,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段不再被任何人使用,并且您想阻止新用户使用它,请考虑用一个reserved 语句替换字段声明。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"
];
}
读取 string_name
选项的 C++ 代码可能如下所示
const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);
请参阅自定义选项,了解如何将自定义选项应用于枚举值和字段。
自定义选项
Protocol Buffers 还允许您定义和使用自己的选项。请注意,这是一个高级特性,大多数人不需要。如果您确实认为需要创建自己的选项,请参阅Proto2 语言指南了解详细信息。请注意,创建自定义选项使用扩展,在 proto3 中仅允许用于自定义选项。
选项保留
选项有一个保留 (retention) 概念,它控制选项是否保留在生成的代码中。选项默认具有运行时保留 (runtime retention),这意味着它们保留在生成的代码中,因此在运行时在生成的描述符池中可见。但是,您可以设置 retention = RETENTION_SOURCE
来指定某个选项(或选项内的字段)不得在运行时保留。这称为源保留 (source retention)。
选项保留是一个高级特性,大多数用户无需担心,但如果您想使用某些选项而无需承担在二进制文件中保留它们的代码大小成本,它会很有用。具有源保留的选项仍然对 protoc
和 protoc
插件可见,因此代码生成器可以使用它们来自定义其行为。
保留可以直接在选项上设置,如下所示
extend google.protobuf.FileOptions {
optional int32 source_retention_option = 1234
[retention = RETENTION_SOURCE];
}
它也可以设置在普通字段上,在这种情况下,只有当该字段出现在选项内部时才会生效
message OptionsMessage {
int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}
您可以设置 retention = RETENTION_RUNTIME
,但这没有效果,因为它是默认行为。当一个消息字段被标记为 RETENTION_SOURCE
时,其全部内容都会被丢弃;其中的字段无法通过尝试设置 RETENTION_RUNTIME
来覆盖它。
注意
截至 Protocol Buffers 22.0,选项保留的支持仍在进行中,目前仅支持 C++ 和 Java。Go 从 1.29.0 开始支持。Python 的支持已完成,但尚未发布版本。选项目标
字段有一个 targets
选项,它控制当字段用作选项时可以应用于的实体类型。例如,如果一个字段具有 targets = TARGET_TYPE_MESSAGE
,那么该字段就不能在枚举(或任何其他非消息实体)上的自定义选项中设置。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];
}
extend google.protobuf.FileOptions {
optional MyOptions file_options = 50000;
}
extend google.protobuf.MessageOptions {
optional MyOptions message_options = 50000;
}
extend google.protobuf.EnumOptions {
optional MyOptions enum_options = 50000;
}
// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";
message MyMessage {
// OK: this field is allowed on both message and enum options
option (message_options).message_and_enum_option = 42;
}
enum MyEnum {
MY_ENUM_UNSPECIFIED = 0;
// Error: file_only_option cannot be set on an enum.
option (enum_options).file_only_option = "xyz";
}
生成您的类
要生成处理 .proto
文件中定义的消息类型所需的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 代码,您需要在 .proto
文件上运行 protocol buffer 编译器 protoc
。如果您尚未安装编译器,请下载软件包并按照 README 中的说明进行操作。对于 Go,您还需要为编译器安装一个特殊的代码生成器插件;您可以在 GitHub 上的golang/protobuf 仓库中找到该插件和安装说明。
Protocol Compiler 的调用方式如下
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
IMPORT_PATH
指定一个目录,用于在解析import
指令时查找.proto
文件。如果省略,则使用当前目录。可以通过多次传递--proto_path
选项来指定多个导入目录。-I=_IMPORT_PATH_
可用作--proto_path
的缩写形式。
注意:相对于其 proto_path
的文件路径在给定的二进制文件中必须全局唯一。例如,如果您有 proto/lib1/data.proto
和 proto/lib2/data.proto
,则不能将这两个文件与 -I=proto/lib1 -I=proto/lib2
一起使用,因为这样无法明确 import "data.proto"
指的是哪个文件。应改用 -Iproto/
,全局名称将是 lib1/data.proto
和 lib2/data.proto
。
如果您正在发布一个库,并且其他用户可能直接使用您的消息,您应该在他们预期使用该库的路径中包含唯一的库名称,以避免文件名冲突。如果您在一个项目中有多个目录,最佳实践是首选将一个 -I
设置为项目的顶级目录。
您可以提供一个或多个输出指令
--cpp_out
在DST_DIR
中生成 C++ 代码。有关更多信息,请参阅C++ 生成的代码参考。--java_out
在DST_DIR
中生成 Java 代码。有关更多信息,请参阅Java 生成的代码参考。--kotlin_out
在DST_DIR
中生成额外的 Kotlin 代码。有关更多信息,请参阅Kotlin 生成的代码参考。--python_out
在DST_DIR
中生成 Python 代码。有关更多信息,请参阅Python 生成的代码参考。--go_out
在DST_DIR
中生成 Go 代码。有关更多信息,请参阅Go 生成的代码参考。--ruby_out
在DST_DIR
中生成 Ruby 代码。有关更多信息,请参阅Ruby 生成的代码参考。--objc_out
在DST_DIR
中生成 Objective-C 代码。有关更多信息,请参阅Objective-C 生成的代码参考。--csharp_out
在DST_DIR
中生成 C# 代码。有关更多信息,请参阅C# 生成的代码参考。--php_out
在DST_DIR
中生成 PHP 代码。有关更多信息,请参阅PHP 生成的代码参考。
为了额外方便,如果
DST_DIR
以.zip
或.jar
结尾,编译器将把输出写入到具有给定名称的单个 ZIP 格式归档文件中。.jar
输出还将按照 Java JAR 规范的要求提供一个清单文件。请注意,如果输出归档文件已存在,它将被覆盖。您必须提供一个或多个
.proto
文件作为输入。可以一次指定多个.proto
文件。尽管文件名称是相对于当前目录的,但每个文件都必须位于其中一个IMPORT_PATH
中,以便编译器可以确定其规范名称。
文件位置
最好不要将 .proto
文件放在与其他语言源代码相同的目录中。考虑在项目根包下为 .proto
文件创建一个子包 proto
。
位置应该是语言无关的
使用 Java 代码时,将相关的 .proto
文件放在与 Java 源代码相同的目录中很方便。但是,如果任何非 Java 代码使用相同的 protos,则路径前缀将不再有意义。因此,通常情况下,将 protos 放在相关的语言无关目录中,例如 //myteam/mypackage
。
此规则的例外情况是,如果 protos 显然仅用于 Java 环境,例如用于测试。
支持的平台
有关以下信息
- 支持的操作系统、编译器、构建系统和 C++ 版本,请参阅基础 C++ 支持策略。
- 支持的 PHP 版本,请参阅支持的 PHP 版本。