语言指南 (版本)

介绍如何在您的项目中使用 Protocol Buffers 语言的 2023 版修订版。

本指南介绍如何使用 protocol buffer 语言来构建您的 protocol buffer 数据,包括 .proto 文件语法以及如何从您的 .proto 文件生成数据访问类。它涵盖了 protocol buffers 语言的 **2023 版**。有关版本在概念上与 proto2 和 proto3 的区别信息,请参阅 Protobuf 版本概述

有关 **proto2** 语法的信息,请参阅 Proto2 语言指南

有关 **proto3** 语法的信息,请参阅 Proto3 语言指南

这是一份参考指南 - 对于使用本文档中描述的许多功能的分步示例,请参阅您选择的语言的 教程

定义消息类型

首先让我们看一个非常简单的例子。假设您想定义一个搜索请求消息格式,其中每个搜索请求都包含一个查询字符串、您感兴趣的特定结果页面以及每页的结果数。这是您用于定义消息类型的 .proto 文件。

edition = "2023";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 文件的首行指定您使用的是 protobuf 语言规范的 2023 版。

    • edition(或 proto2/proto3 的 syntax)必须是文件中的第一行非空、非注释行。
    • 如果未指定 editionsyntax,protocol buffer 编译器将假设您使用的是 proto2
  • SearchRequest 消息定义指定了三个字段(名称/值对),每个字段对应于您想要包含在此类型消息中的每条数据。每个字段都有一个名称和一个类型。

指定字段类型

在前面的示例中,所有字段都是 标量类型:两个整数 (page_numberresults_per_page) 和一个字符串 (query)。您还可以为字段指定 枚举 和复合类型(如其他消息类型)。

分配字段编号

您必须为消息定义中的每个字段指定一个介于 1536,870,911 之间的数字,并遵循以下限制

  • 给定的数字在该消息的所有字段中 **必须唯一**。
  • 字段编号 19,00019,999 保留用于 Protocol Buffers 实现。如果您在消息中使用这些保留的字段编号之一,protocol buffer 编译器将发出警告。
  • 您不能使用任何先前 保留 的字段编号或已分配给 扩展 的任何字段编号。

一旦您的消息类型投入使用,此编号 **不能更改**,因为它在 消息线格式 中标识字段。“更改”字段编号等同于删除该字段并使用相同类型但新的编号创建一个新字段。请参阅 删除字段 以了解如何正确执行此操作。

字段编号 **不应重复使用**。切勿从 保留 列表中取出字段编号以供新的字段定义重复使用。请参阅 重复使用字段编号的后果

您应该将字段编号 1 到 15 用于最常设置的字段。较低的字段编号值在线格式中占用更少的空间。例如,范围 1 到 15 内的字段编号需要一个字节来编码。范围 16 到 2047 内的字段编号需要两个字节。您可以在 Protocol Buffer 编码 中了解更多信息。

重复使用字段编号的后果

重复使用字段编号会使解码线格式消息变得模棱两可。

protobuf 线格式简洁,无法检测使用一个定义编码并使用另一个定义解码的字段。

使用一个定义编码字段,然后使用不同的定义解码同一字段可能导致

  • 开发人员花费时间调试
  • 解析/合并错误(最佳情况)
  • PII/SPII 泄漏
  • 数据损坏

字段编号重复使用的常见原因

  • 重新编号字段(有时这样做是为了获得字段更美观的数字顺序)。重新编号实际上会删除并重新添加所有参与重新编号的字段,从而导致线格式的不兼容更改。

  • 删除字段且未 保留 该编号以防止将来重复使用。

    • 由于以下几个原因,在使用 扩展字段 时,这很容易出错。 扩展声明 提供了一种保留扩展字段的机制。

字段编号限制为 29 位而不是 32 位,因为三位用于指定字段的线格式。有关详细信息,请参阅 编码主题

指定字段基数

消息字段可以是以下之一

  • 单数:

    单数字段没有显式的基数标签。它有两种可能的状态

    • 字段已设置,并包含显式设置或从线格式解析的值。它将被序列化到线格式。
    • 字段未设置,并将返回默认值。它不会被序列化到线格式。

    您可以检查该值是否已显式设置。

    已迁移到版本的 Proto3 隐式字段将使用 field_presence 特性并将其设置为 IMPLICIT 值。

    已迁移到版本的 Proto2 required 字段也将使用 field_presence 特性,但将其设置为 LEGACY_REQUIRED

  • repeated:此字段类型可以在格式良好的消息中重复零次或多次。重复值的顺序将保留。

  • map:这是一个配对的键/值字段类型。有关此字段类型的更多信息,请参阅 映射

重复字段默认情况下被打包

在 proto 版本中,标量数字类型的 repeated 字段默认使用 packed 编码。

您可以在 Protocol Buffer 编码 中了解有关 packed 编码的更多信息。

格式良好的消息

当应用于 protobuf 消息时,“格式良好”一词指的是序列化/反序列化的字节。protoc 解析器验证给定的 proto 定义文件是否可解析。

单数字段可以在线格式字节中出现多次。解析器将接受输入,但只有该字段的最后一个实例可通过生成的绑定访问。有关此主题的更多信息,请参阅 最后一次获胜

添加更多消息类型

可以在单个 .proto 文件中定义多种消息类型。如果您正在定义多个相关消息,这将非常有用 - 因此,例如,如果您想定义与 SearchResponse 消息类型相对应的回复消息格式,您可以将其添加到同一个 .proto

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

message SearchResponse {
 ...
}

组合消息会导致膨胀 虽然可以在单个 .proto 文件中定义多种消息类型(例如消息、枚举和服务),但这也会导致在单个文件中定义大量具有不同依赖项的消息时出现依赖项膨胀。建议每个 .proto 文件包含尽可能少的消息类型。

添加注释

.proto 文件中添加注释

  • 在 .proto 代码元素之前的行上首选 C/C++/Java 行尾样式注释“//”

  • 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 上运行协议缓冲区编译器时,编译器会生成您选择的语言中的代码,您需要使用这些代码来处理您在文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流中解析消息。

  • 对于C++,编译器从每个 .proto 生成一个 .h.cc 文件,每个文件中描述的消息类型都有一个类。
  • 对于Java,编译器生成一个 .java 文件,其中包含每个消息类型的类,以及一个用于创建消息类实例的特殊 Builder 类。
  • 对于Kotlin,除了生成的 Java 代码外,编译器还为每个消息类型生成一个 .kt 文件,其中包含改进的 Kotlin API。这包括一个简化创建消息实例的 DSL、一个可空字段访问器和一个复制函数。
  • Python有点不同——Python 编译器生成一个模块,其中包含 .proto 中每个消息类型的静态描述符,然后将其与元类一起使用,在运行时创建必要的 Python 数据访问类。
  • 对于Go,编译器生成一个 .pb.go 文件,其中包含文件中每个消息类型的类型。
  • 对于Ruby,编译器生成一个 .rb 文件,其中包含一个包含消息类型的 Ruby 模块。
  • 对于Objective-C,编译器从每个 .proto 生成一个 pbobjc.hpbobjc.m 文件,每个文件中描述的消息类型都有一个类。
  • 对于C#,编译器从每个 .proto 生成一个 .cs 文件,每个文件中描述的消息类型都有一个类。
  • 对于PHP,编译器为每个文件中描述的消息类型生成一个 .php 消息文件,并为每个编译的 .proto 文件生成一个 .php 元数据文件。元数据文件用于将有效的消息类型加载到描述符池中。
  • 对于Dart,编译器生成一个 .pb.dart 文件,其中包含文件中每个消息类型的类。

您可以通过按照您选择的语言的教程了解更多关于使用每种语言的 API 的信息。有关更多 API 详细信息,请参阅相关的API 参考

标量值类型

标量消息字段可以具有以下类型之一——表格显示了在 .proto 文件中指定的类型,以及在自动生成的类中的对应类型

.proto 类型注释C++ 类型Java/Kotlin 类型[1]Python 类型[3]Go 类型Ruby 类型C# 类型PHP 类型Dart 类型Rust 类型
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32使用可变长度编码。对负数的编码效率低下——如果您的字段可能包含负值,请改用 sint32。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
int64使用可变长度编码。对负数的编码效率低下——如果您的字段可能包含负值,请改用 sint64。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32使用可变长度编码。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(根据需要)uintintegerintu32
uint64使用可变长度编码。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32使用可变长度编码。有符号 int 值。这些比常规 int32 更有效地编码负数。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
sint64使用可变长度编码。有符号 int 值。这些比常规 int64 更有效地编码负数。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32始终为四个字节。如果值通常大于 228,则比 uint32 更有效。uint32int[2]int/long[4]uint32Fixnum 或 Bignum(根据需要)uintintegerintu32
fixed64始终为八个字节。如果值通常大于 256,则比 uint64 更有效。uint64long[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32始终为四个字节。int32intintint32Fixnum 或 Bignum(根据需要)intintegerinti32
sfixed64始终为八个字节。int64longint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且不能超过 232stringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytes可以包含任何任意字节序列,最长不超过 232stringByteStringstr (Python 2)
bytes (Python 3)
[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[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。

您可以在协议缓冲区编码中了解更多关于序列化消息时如何编码这些类型的信息。

默认字段值

当解析消息时,如果编码的消息字节不包含特定字段,则在解析的对象中访问该字段将返回该字段的默认值。默认值是特定于类型的

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数字类型,默认值为零。
  • 对于消息字段,该字段未设置。其确切值取决于语言。有关详细信息,请参阅生成的代码指南
  • 对于枚举,默认值为**第一个定义的枚举值**,该值必须为 0。请参阅枚举默认值

重复字段的默认值为 empty(通常是相应语言中的空列表)。

映射字段的默认值为 empty(通常是相应语言中的空映射)。

覆盖默认标量值

在 protobuf 版本中,您可以为单数非消息字段指定显式默认值。例如,假设您希望为 SearchRequest.result_per_page 字段提供默认值 10

int32 result_per_page = 3 [default = 10];

如果发送方未指定 result_per_page,则接收方将观察以下状态

  • result_per_page 字段不存在。也就是说,has_result_per_page()(hazzer 方法)方法将返回 false
  • result_per_page 的值(从“getter”返回)为 10

如果发送方确实发送了 result_per_page 的值,则会忽略默认值 10,并从“getter”返回发送方的值。

有关默认值在生成代码中如何工作的更多详细信息,请参阅您选择的语言的生成的代码指南

无法为将 field_presence 功能设置为 IMPLICIT 的字段指定显式默认值。

枚举

在定义消息类型时,您可能希望其字段之一仅具有预定义值列表中的一个值。例如,假设您想为每个 SearchRequest 添加一个 corpus 字段,其中语料库可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以通过向消息定义添加一个 enum 并为每个可能的值添加一个常量来非常简单地做到这一点。

在以下示例中,我们添加了一个名为 Corpusenum,其中包含所有可能的值,以及一个类型为 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,因为这是枚举中定义的第一个值。

在 2023 版中,枚举定义中定义的第一个值**必须**值为零,并且应命名为 ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。这是因为

  • 零值需要成为第一个元素,以与proto2语义兼容,在 proto2 语义中,除非显式指定了其他值,否则第一个枚举值是默认值。
  • 必须存在一个零值,以与proto3语义兼容,在 proto3 语义中,零值用作使用此枚举类型的所有隐式存在字段的默认值。

还建议此第一个默认值除了“此值未指定”之外没有其他语义含义。

类似于SearchRequest.corpus字段的枚举字段的默认值可以通过以下方式显式覆盖

  Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

如果枚举类型已使用option features.enum_type = CLOSED;从 proto2 迁移,则对枚举中的第一个值没有限制。不建议更改此类枚举的第一个值,因为这将更改使用该枚举类型的任何字段的默认值,而无需显式字段默认值。

枚举值别名

您可以通过为不同的枚举常量分配相同的值来定义别名。为此,您需要将allow_alias选项设置为true。否则,协议缓冲区编译器在找到别名时会生成警告消息。尽管在反序列化期间所有别名值均有效,但在序列化时始终使用第一个值。

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值在线上使用varint 编码,因此负值效率低下,因此不建议使用。您可以在消息定义内定义enum,如前面的示例所示,或者在外部定义 - 这些enum可以在.proto文件中的任何消息定义中重复使用。您还可以使用在一个消息中声明的enum类型作为另一个消息中字段的类型,使用语法_MessageType_._EnumType_

当您在使用enum.proto上运行协议缓冲区编译器时,生成的代码将具有 Java、Kotlin 或 C++ 的相应enum,或者 Python 的特殊EnumDescriptor类,该类用于在运行时生成的类中创建一组带有整数值的符号常量。

在反序列化期间,将保留消息中未识别的枚举值,尽管消息反序列化时如何表示取决于语言。在支持具有超出指定符号范围的值的开放枚举类型的语言(例如 C++ 和 Go)中,未知枚举值仅作为其底层整数表示形式存储。在具有封闭枚举类型的语言(如 Java)中,枚举中的一个情况用于表示未识别的值,并且可以使用特殊访问器访问底层整数。在这两种情况下,如果序列化消息,则未识别的值仍将与消息一起序列化。

有关如何在应用程序中使用消息enum的更多信息,请参阅您所选语言的生成代码指南

保留值

如果您更新枚举类型以完全删除枚举条目或将其注释掉,则将来用户可以在对类型进行自己的更新时重复使用数值。如果他们以后加载相同.proto的旧实例,这可能会导致严重问题,包括数据损坏、隐私错误等。确保这不会发生的一种方法是指定已删除条目的数值(和/或名称,这也会导致 JSON 序列化出现问题)为reserved。如果将来任何用户尝试使用这些标识符,协议缓冲区编译器将发出警告。您可以指定保留的数值范围使用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文件中定义怎么办?

您可以通过导入它们来使用其他.proto文件中的定义。要导入另一个.proto的定义,请在文件顶部添加导入语句

import "myproject/other_protos.proto";

默认情况下,您只能使用直接导入的.proto文件中的定义。但是,有时您可能需要将.proto文件移动到新位置。您可以使用占位符.proto文件代替直接移动.proto文件并在单个更改中更新所有调用站点,以使用import public概念将所有导入转发到新位置。

请注意,公共导入功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 静态反射的 C++ 目标中不可用。

import public依赖项可以被导入包含import public语句的 proto 的任何代码传递依赖。例如

// 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 和 proto3 消息类型

可以导入proto2proto3消息类型并在您的版本 2023 消息中使用它们,反之亦然。

嵌套类型

您可以在其他消息类型内部定义和使用消息类型,如下例所示 - 在此处,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;
    }
  }
}

更新消息类型

如果现有消息类型不再满足您的所有需求 - 例如,您希望消息格式具有额外的字段 - 但您仍然希望使用使用旧格式创建的代码,请不要担心!当您使用二进制线格式时,更新消息类型而不会破坏任何现有代码非常简单。

检查Proto 最佳实践和以下规则

  • 不要更改任何现有字段的字段编号。“更改”字段编号等效于删除字段并添加一个具有相同类型的新字段。如果要重新编号字段,请参阅删除字段的说明。
  • 如果添加新字段,则使用您的“旧”消息格式序列化的任何消息仍可以由您的新生成代码解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的邮件进行交互。同样,由您的新代码创建的消息可以由您的旧代码解析:旧二进制文件在解析时只会忽略新字段。有关详细信息,请参阅未知字段部分。
  • 可以删除字段,只要在更新的消息类型中不再使用字段编号即可。您可能希望重命名字段,例如添加前缀“OBSOLETE_”,或使字段编号保留,以便将来使用您的.proto的用户不会意外地重复使用该编号。
  • int32uint32int64uint64bool都兼容 - 这意味着您可以将字段从这些类型中的任何一个更改为另一个,而不会破坏向前或向后兼容性。如果从线上解析出的数字不适合相应的类型,您将获得与在 C++ 中将数字强制转换为该类型相同的效果(例如,如果将 64 位数字读为 int32,它将被截断为 32 位)。
  • sint32sint64彼此兼容,但与其他整数类型兼容。
  • 只要字节是有效的 UTF-8,stringbytes就是兼容的。
  • 如果字节包含消息的编码实例,则嵌入式消息与bytes兼容。
  • fixed32sfixed32兼容,fixed64sfixed64兼容。
  • 对于stringbytes和消息字段,单数与repeated兼容。给定重复字段的序列化数据作为输入,如果它是原始类型字段,则期望此字段为单数的客户端将采用最后一个输入值;如果它是消息类型字段,则将合并所有输入元素。请注意,这对于数值类型(包括布尔值和枚举)通常是不安全的。数值类型的重复字段默认以打包格式序列化,当期望单数字段时,将无法正确解析。
  • 就线格式而言,enumint32uint32int64uint64兼容(请注意,如果值不适合,则会将其截断)。但是,请注意,当反序列化消息时,客户端代码可能会以不同的方式处理它们:例如,将保留消息中未识别的enum值,但消息反序列化时如何表示取决于语言。整数字段始终只保留其值。
  • 将单个optional字段或扩展更改为新的oneof的成员是二进制兼容的,但是对于某些语言(特别是 Go),生成的代码的 API 将以不兼容的方式更改。由于这个原因,Google 不在其公共 API 中进行此类更改,如AIP-180中所述。对于源代码兼容性相同的事项,如果确定没有代码一次设置多个字段,则将多个字段移动到新的oneof可能是安全的。将字段移动到现有的oneof是不安全的。同样,将单个字段oneof更改为optional字段或扩展名是安全的。
  • map<K, V>和相应的repeated消息字段之间更改字段是二进制兼容的(有关消息布局和其他限制,请参见下面的映射)。但是,更改的安全性取决于应用程序:在反序列化和重新序列化消息时,使用repeated字段定义的客户端将产生语义上相同的结果;但是,使用map字段定义的客户端可能会重新排序条目并删除具有重复键的条目。

未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析由具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

版本消息保留未知字段并在解析和序列化输出期间包含它们,这与 proto2 和 proto3 行为匹配。

保留未知字段

某些操作会导致未知字段丢失。例如,如果您执行以下操作之一,则未知字段将丢失

  • 将 proto 序列化为 JSON。
  • 遍历消息中的所有字段以填充新消息。

为避免丢失未知字段,请执行以下操作

  • 使用二进制;避免使用文本格式进行数据交换。
  • 使用面向消息的 API(例如CopyFrom()MergeFrom())来复制数据,而不是逐字段复制

TextFormat 是一种特殊情况。序列化到 TextFormat 会使用字段编号打印未知字段。但是,如果存在使用字段编号的条目,则将 TextFormat 数据解析回二进制 proto 将失败。

扩展

扩展是在其容器消息之外定义的字段;通常在与容器消息的.proto文件不同的.proto文件中。

为什么要使用扩展?

使用扩展有两个主要原因

  • 包含消息的.proto文件将包含更少的导入/依赖项。这可以提高构建速度,打破循环依赖,并促进松耦合。扩展在这方面非常有效。
  • 允许系统将数据附加到包含消息中,而无需额外的依赖和协调。扩展不是一个很好的解决方案,因为字段编号空间有限,并且存在重复使用字段编号的后果。如果您的用例需要为大量扩展提供非常低的协调性,请考虑使用Any消息类型

扩展示例

让我们看一个扩展的例子。

// file kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
  // Video is a message imported from kittens/video.proto
  repeated Video kitten_videos = 126;
}

请注意,定义扩展的文件(kittens/video_ext.proto)导入了包含消息的文件(media/user_content.proto)。

包含消息必须为扩展预留一部分字段编号。

// file media/user_content.proto

package media;

// A container message to hold stuff that a user has created.
message UserContent {
  // Set verification to `DECLARATION` to enforce extension declarations for all
  // extensions in this range.
  extensions 100 to 199 [verification = DECLARATION];
}

包含消息的文件(media/user_content.proto)定义了消息UserContent,它为扩展预留了字段编号[100到199]。建议为该范围设置verification = DECLARATION,以要求为其所有扩展声明。

当添加新的扩展(kittens/video_ext.proto)时,应在UserContent中添加相应的声明,并删除verification

// A container message to hold stuff that a user has created.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    },
    // Ensures all field numbers in this extension range are declarations.
    verification = DECLARATION
  ];
}

UserContent声明字段编号126将由一个repeated扩展字段使用,该字段的完全限定名称为.kittens.kitten_videos,完全限定类型为.kittens.Video。要了解有关扩展声明的更多信息,请参阅扩展声明

请注意,包含消息的文件(media/user_content.proto)**不**导入小猫视频扩展定义(kittens/video_ext.proto)。

扩展字段的线格式编码与具有相同字段编号、类型和基数的标准字段的编码没有区别。因此,可以安全地将标准字段从其容器移出以成为扩展,或者将扩展字段移入其容器消息作为标准字段,只要字段编号、类型和基数保持不变。

但是,由于扩展是在包含消息外部定义的,因此不会生成专门的访问器来获取和设置特定的扩展字段。对于我们的示例,protobuf编译器**不会生成**AddKittenVideos()GetKittenVideos()访问器。相反,扩展是通过参数化函数访问的,例如:HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()

在C++中,它看起来像这样

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

定义扩展范围

如果您是包含消息的所有者,则需要为消息的扩展定义一个扩展范围。

分配给扩展字段的字段编号不能重复用于标准字段。

在定义扩展范围后,可以安全地扩展它。一个好的默认值是分配1000个相对较小的数字,并使用扩展声明密集地填充该空间。

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

在实际扩展之前添加扩展声明的范围时,应添加verification = DECLARATION以强制使用声明来使用此新范围。一旦添加了实际声明,就可以删除此占位符。

可以安全地将现有扩展范围拆分为覆盖相同总范围的单独范围。这对于将旧版消息类型迁移到扩展声明可能是必要的。例如,在迁移之前,范围可能定义为

message LegacyMessage {
  extensions 1000 to max;
}

迁移后(拆分范围)可以是

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

增加起始字段编号或减小结束字段编号以移动或缩小扩展范围是不安全的。这些更改可能会使现有的扩展失效。

对于大多数原型实例中填充的标准字段,最好使用字段编号1到15。不建议将这些编号用于扩展。

如果您的编号约定可能涉及扩展具有非常大的字段编号,则可以使用max关键字指定您的扩展范围达到最大可能的字段编号。

message Foo {
  extensions 1000 to max;
}

max是229 - 1,或536,870,911。

选择扩展编号

扩展只是可以在其包含消息外部指定的字段。所有相同的分配字段编号规则都适用于扩展字段编号。重复使用字段编号的后果也适用于重复使用扩展字段编号。

如果包含消息使用扩展声明,则选择唯一的扩展字段编号很简单。在定义新的扩展时,选择高于包含消息中定义的最高扩展范围的所有其他声明的最低字段编号。例如,如果包含消息定义如下

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Container的下一个扩展应添加一个新的声明,编号为525000002

未经验证的扩展编号分配(不推荐)

包含消息的所有者可以选择放弃扩展声明,转而使用他们自己的未经验证的扩展编号分配策略。

未经验证的分配方案使用protobuf生态系统外部的机制在选定的扩展范围内分配扩展字段编号。一个例子可能是使用monorepo的提交编号。从protobuf编译器的角度来看,此系统是“未经验证的”,因为无法检查扩展是否使用了正确获取的扩展字段编号。

与扩展声明等已验证系统相比,未经验证系统的优势在于能够在不与包含消息所有者协调的情况下定义扩展。

未经验证系统的缺点是protobuf编译器无法保护参与者免于重复使用扩展字段编号。

**不建议使用未经验证的扩展字段编号分配策略**,因为重复使用字段编号的后果将落在消息的所有扩展者身上(不仅仅是未遵循建议的开发人员)。如果您的用例需要非常低的协调性,请考虑使用Any消息

未经验证的扩展字段编号分配策略仅限于1到524,999,999的范围。525,000,000及以上的字段编号只能与扩展声明一起使用。

指定扩展类型

扩展可以是任何字段类型,除了oneofmap

嵌套扩展(不推荐)

您可以在另一个消息的范围内声明扩展

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    int32 likes_count = 111;
  }
  ...
}

在这种情况下,访问此扩展的C++代码为

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

换句话说,唯一的效果是likes_count是在puppies.Photo的范围内定义的。

这是一个常见的混淆来源:在消息类型内部嵌套声明extend并不意味着外部类型和扩展类型之间有任何关系。特别是,前面的示例并不意味着PhotoUserProfile的任何子类。它仅仅意味着符号likes_count是在Photo的范围内声明的;它只是一个静态成员。

一个常见的模式是在扩展字段类型的范围内定义扩展 - 例如,这是一个对类型为puppies.Photomedia.UserContent的扩展,其中扩展定义为Photo的一部分

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    Photo puppy_photo = 127;
  }
  ...
}

但是,没有要求具有消息类型的扩展必须在该类型内部定义。您还可以使用标准定义模式

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  Photo puppy_photo = 127;
}

标准(文件级)语法是首选,以避免混淆。嵌套语法通常会被不熟悉扩展的用户误认为是子类化。

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 ...
  }
}

如果要将包含的消息限制为少量类型,并在将新类型添加到列表之前需要权限,请考虑使用扩展扩展声明而不是Any消息类型。

Oneof

如果您的消息包含许多单数字段,并且在同一时间最多只有一个字段会被设置,则可以使用oneof功能来强制执行此行为并节省内存。

Oneof字段类似于单数字段,除了oneof中的所有字段共享内存,并且最多只能设置一个字段。设置oneof的任何成员会自动清除所有其他成员。您可以使用特殊的case()WhichOneof()方法检查oneof中设置了哪个值(如果有),具体取决于您选择的语言。

请注意,如果设置了多个值,则由proto中的顺序确定的最后一个设置值将覆盖所有先前的值

oneof字段的字段编号在包含消息中必须唯一。

使用 Oneof

要在您的.proto中定义oneof,请使用oneof关键字后跟您的oneof名称,在本例中为test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

然后将您的oneof字段添加到oneof定义中。您可以添加任何类型的字段,除了map字段和repeated字段。如果需要将重复字段添加到oneof,可以使用包含重复字段的消息。

在生成的代码中,oneof字段具有与常规字段相同的getter和setter。您还可以获得一个特殊的用于检查oneof中设置了哪个值(如果有)的方法。您可以在相关的API参考中找到有关所选语言的oneof API的更多信息。

Oneof 特性

  • 设置oneof字段将自动清除oneof的所有其他成员。因此,如果您设置了几个oneof字段,则只有您设置的最后一个字段仍将具有值。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_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.has_name());
    
  • 如果解析器在网络上遇到相同oneof的多个成员,则仅使用解析的消息中看到的最后一个成员。

  • 扩展不支持oneof。

  • oneof不能是repeated

  • 反射API适用于oneof字段。

  • 如果将oneof字段设置为默认值(例如,将int32 oneof字段设置为0),则该oneof字段的“case”将被设置,并且该值将在网络上序列化。

  • 如果您使用的是C++,请确保您的代码不会导致内存崩溃。以下示例代码将崩溃,因为sub_message已通过调用set_name()方法删除。

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • 同样在C++中,如果您Swap()两个包含oneof的消息,则每条消息最终将包含另一条消息的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(msg2.has_name());
    

向后兼容性问题

在添加或删除oneof字段时要小心。如果检查oneof的值返回None/NOT_SET,则可能意味着oneof尚未设置或已设置为oneof的不同版本中的字段。无法区分两者,因为无法知道网络上的未知字段是否是oneof的成员。

标签重用问题

  • 将单个字段移入或移出oneof:消息序列化和解析后,您可能会丢失一些信息(某些字段将被清空)。但是,您可以安全地将单个字段移入一个新的oneof,并且如果已知只有一个字段会被设置,则可以移动多个字段。有关更多详细信息,请参阅更新消息类型
  • 删除一个oneof字段并重新添加它:这可能会在消息序列化和解析后清除您当前设置的oneof字段。
  • 拆分或合并oneof:这与移动单个字段存在类似的问题。

映射

如果您想在数据定义中创建关联映射,protocol buffers提供了一个方便的快捷语法

map<key_type, value_type> map_field = N;

…其中key_type可以是任何整数或字符串类型(因此,任何标量类型,除了浮点类型和bytes)。请注意,枚举或proto消息对于key_type无效。value_type可以是任何类型,除了另一个映射。

例如,如果您想创建一个项目的映射,其中每个Project消息都与一个字符串键关联,您可以这样定义它

map<string, Project> projects = 3;

映射特性

  • 映射不支持扩展。
  • 映射字段不能是repeated
  • 映射值的线格式排序和映射迭代排序未定义,因此您不能依赖映射项以特定顺序排列。
  • 当为.proto生成文本格式时,映射按键排序。数字键按数字排序。
  • 在从网络解析或合并时,如果存在重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复的键,解析可能会失败。
  • 如果您为映射字段提供键但不提供值,则字段序列化时的行为取决于语言。在C++、Java、Kotlin和Python中,会序列化该类型的默认值,而在其他语言中,则不会序列化任何内容。
  • 在与映射foo相同的范围内,不能存在符号FooEntry,因为FooEntry已被映射的实现使用。

生成的映射API目前可用于所有支持的语言。您可以在相关的API参考中找到有关所选语言的映射API的更多信息。

向后兼容性

映射语法等效于网络上的以下内容,因此不支持映射的protocol buffers实现仍然可以处理您的数据

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持映射的protocol buffers实现都必须同时生成和接受可以被早期定义接受的数据。

您可以向.proto文件添加可选的package说明符,以防止protocol message类型之间的名称冲突。

package foo.bar;
message Open { ... }

然后,您可以在定义消息类型的字段时使用包说明符

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

包说明符影响生成代码的方式取决于您选择的语言

  • C++中,生成的类被包装在C++命名空间中。例如,Open将位于foo::bar命名空间中。
  • JavaKotlin中,包用作Java包,除非您在.proto文件中显式提供option java_package
  • 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无法移植到其他语言。

包和名称解析

protocol buffer语言中的类型名称解析的工作方式类似于C++:首先搜索最内部的作用域,然后搜索下一个内部作用域,依此类推,每个包都被认为与其父包“内部”。前导的‘.’(例如,.foo.bar.Baz)表示从最外部的作用域开始。

protocol buffer编译器通过解析导入的.proto文件来解析所有类型名称。每种语言的代码生成器都知道如何在该语言中引用每种类型,即使它具有不同的作用域规则。

定义服务

如果您想将您的消息类型与RPC(远程过程调用)系统一起使用,您可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将在您选择的语言中生成服务接口代码和存根。因此,例如,如果您想定义一个RPC服务,该服务具有一个采用SearchRequest并返回SearchResponse的方法,您可以在.proto文件中按如下方式定义它

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

与protocol buffers一起使用的最简单的RPC系统是gRPC:一个在Google开发的语言和平台中立的开源RPC系统。gRPC与protocol buffers配合得特别好,并允许您使用特殊的protocol buffer编译器插件直接从.proto文件生成相关的RPC代码。

如果您不想使用gRPC,也可以将protocol buffers与您自己的RPC实现一起使用。您可以在Proto2语言指南中找到更多相关信息。

还有一些正在进行的第三方项目正在开发用于Protocol Buffers的RPC实现。有关我们已知项目的链接列表,请参阅第三方插件维基页面

JSON 映射

标准protobuf二进制线格式是使用protobufs的两个系统之间通信的首选序列化格式。对于与使用JSON而不是protobuf线格式的系统进行通信,Protobuf支持ProtoJSON中的规范编码。

选项

.proto文件中的单个声明可以用许多选项进行注释。选项不会更改声明的整体含义,但可能会影响它在特定上下文中的处理方式。可用选项的完整列表在/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(文件选项):可以设置为SPEEDCODE_SIZELITE_RUNTIME。这会影响C++和Java代码生成器(以及可能的第三方生成器),方式如下

    • SPEED(默认):protocol buffer编译器将生成用于序列化、解析和对消息类型执行其他常见操作的代码。此代码经过高度优化。

    • CODE_SIZE: 协议缓冲区编译器将生成最小的类,并依赖于共享的、基于反射的代码来实现序列化、解析和各种其他操作。因此,生成的代码将比使用SPEED时小得多,但操作速度会较慢。类仍然会完全实现与SPEED模式下相同的公共 API。此模式最适用于包含大量.proto文件的应用程序,并且不需要所有文件都具有极快的速度。
    • LITE_RUNTIME: 协议缓冲区编译器将生成仅依赖于“精简版”运行时库(libprotobuf-lite而不是libprotobuf)的类。精简版运行时库比完整库小得多(大约小一个数量级),但省略了描述符和反射等某些功能。这对于在移动电话等受限平台上运行的应用程序特别有用。编译器仍然会像在SPEED模式下一样生成所有方法的快速实现。生成的类将仅在每种语言中实现MessageLite接口,该接口仅提供完整Message接口中方法的一个子集。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_servicesjava_generic_servicespy_generic_services(文件选项):**泛型服务已弃用。**协议缓冲区编译器是否应分别基于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分配

  • objc_class_prefix(文件选项):设置Objective-C类前缀,该前缀将附加到从此.proto生成的所有的Objective-C类和枚举中。没有默认值。您应该使用3到5个大写字符的前缀,如Apple推荐的那样。请注意,所有2个字母的前缀都被Apple保留。

  • packed(字段选项):在protobuf版本中,此选项锁定为true。要使用未打包的线格式,您可以使用版本功能覆盖此选项。这提供了与2.3.0版之前的解析器(很少需要)的兼容性,如下例所示

    repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
    
  • deprecated(字段选项):如果设置为true,则表示该字段已弃用,新代码不应使用它。在大多数语言中,这实际上没有任何效果。在Java中,它将成为@Deprecated注释。对于C++,clang-tidy将在使用已弃用字段时生成警告。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致在编译尝试使用该字段的代码时发出警告。如果该字段没有被任何人使用,并且您希望阻止新用户使用它,请考虑用reserved语句替换字段声明。

    int32 old_field = 6 [deprecated = true];
    

枚举值选项

支持枚举值选项。您可以使用deprecated选项指示不再应使用某个值。您还可以使用扩展创建自定义选项。

以下示例显示了添加这些选项的语法

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  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语言指南以获取详细信息。请注意,创建自定义选项使用扩展

选项保留

选项具有保留的概念,该概念控制选项是否保留在生成的代码中。选项默认具有运行时保留,这意味着它们保留在生成的代码中,因此在生成的描述符池中在运行时可见。但是,您可以设置retention = RETENTION_SOURCE以指定选项(或选项内的字段)不得在运行时保留。这称为源保留

选项保留是一项高级功能,大多数用户不必担心,但如果您想在不支付将选项保留在二进制文件中的代码大小成本的情况下使用某些选项,它可能会很有用。具有源保留的选项仍然对protocprotoc插件可见,因此代码生成器可以使用它们来自定义其行为。

保留可以像这样直接设置在选项上

extend google.protobuf.FileOptions {
  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来覆盖这一点。

选项目标

字段具有一个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 {
  MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  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文件上运行协议缓冲区编译器protoc。如果您尚未安装编译器,请下载软件包并按照README中的说明进行操作。对于Go,您还需要为编译器安装一个特殊的代码生成器插件;您可以在GitHub上的golang/protobuf存储库中找到此插件及其安装说明。

协议编译器调用如下所示

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的简写形式。

  • 您可以提供一个或多个输出指令

    为了方便起见,如果DST_DIR.zip.jar结尾,编译器会将输出写入具有给定名称的单个ZIP格式归档文件。.jar输出也将根据Java JAR规范的要求获得清单文件。请注意,如果输出归档文件已存在,它将被覆盖。

  • 您必须提供一个或多个.proto文件作为输入。可以同时指定多个.proto文件。尽管文件相对于当前目录命名,但每个文件都必须驻留在IMPORT_PATH之一中,以便编译器可以确定其规范名称。

文件位置

最好不要将.proto文件与其他语言源文件放在同一目录中。考虑为.proto文件创建一个子包proto,位于项目的根包下。

位置应与语言无关

在使用Java代码时,将相关的.proto文件与Java源文件放在同一目录中非常方便。但是,如果任何非Java代码曾经使用相同的proto,则路径前缀将不再有意义。因此,通常,将proto放在相关的与语言无关的目录中,例如//myteam/mypackage

此规则的例外情况是当明确知道proto仅在Java上下文中使用时,例如用于测试。

支持的平台

有关