1. 定义消息类型 {#simple}
    1. 指定字段类型 {#specifying-types}
    2. 分配字段编号 {#assigning}
      1. 重用字段编号的后果 {#consequences}
    3. 指定字段基数 {#field-labels}
      1. 重复字段默认打包 {#use-packed}
      2. 消息类型字段始终具有字段存在性 {#field-presence}
      3. 格式良好的消息 {#well-formed}
    4. 添加更多消息类型 {#adding-types}
    5. 添加注释 {#adding-comments}
    6. 删除字段 {#deleting}
      1. 保留字段编号 {#reserved-field-numbers}
      2. 保留字段名称 {#reserved-field-names}
    7. .proto 生成什么? {#generated}
  2. 标量值类型 {#scalar}
  3. 默认字段值 {#default}
  4. 枚举 {#enum}
    1. 枚举默认值 {#enum-default}
    2. 枚举值别名 {#enum-aliases}
    3. 保留值 {#reserved}
  5. 使用其他消息类型 {#other}
    1. 导入定义 {#importing}
    2. 使用 proto2 消息类型 {#proto2}
  6. 嵌套类型 {#nested}
  7. 更新消息类型 {#updating}
  8. 未知字段 {#unknowns}
    1. 保留未知字段 {#retaining}
  9. Any {#any}
  10. Oneof {#oneof}
    1. 使用 Oneof {#using-oneof}
    2. Oneof 特性 {#oneof-features}
    3. 向后兼容性问题 {#backward}
      1. 标签重用问题 {#reuse}
  11. 映射 {#maps}
    1. 映射特性 {#maps-features}
    2. 向后兼容性 {#backwards}
  12. 包 {#packages}
    1. 包和名称解析 {#name-resolution}
  13. 定义服务 {#services}
  14. JSON 映射 {#json}
  15. 选项 {#options}
    1. 枚举值选项 {#enum-value-options}
    2. 自定义选项 {#customoptions}
    3. 选项保留 {#option-retention}
    4. 选项目标 {#option-targets}
  16. 生成类 {#generating}
  17. 文件位置 {#location}
    1. 位置应与语言无关 {#location-language-agnostic}
  18. 支持的平台 {#platforms}
本文翻译自 Language Guide (proto 3) | Protocol Buffers Documentation

本指南介绍如何使用 Protocol Buffer 语言构建协议缓冲区数据,包括 .proto 文件语法以及如何从 .proto 文件生成数据访问类。它涵盖了 Protocol Buffers 语言的 proto3 版本。

有关 editions 语法的信息,请参阅 Protobuf Editions 语言指南

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

这是参考指南——如需查看使用本文档中描述的多种功能的逐步示例,请参阅您所选语言的教程

定义消息类型 {#simple}

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

syntax = "proto3";

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

指定字段类型 {#specifying-types}

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

分配字段编号 {#assigning}

您必须为消息定义中的每个字段分配 1536,870,911 之间的数字,并遵守以下限制:

此编号在消息类型投入使用后无法更改,因为它在消息线格式中标识字段。"更改"字段编号等同于删除该字段并使用相同类型但新编号创建新字段。有关正确操作方法,请参阅删除字段

字段编号永远不应重用。切勿将字段编号从保留列表中取出以用于新字段定义。请参阅重用字段编号的后果

对于最常设置的字段,应使用字段编号 1 到 15。较低的字段编号值在线格式中占用更少空间。例如,范围 1 到 15 的字段编号编码时占用一个字节。范围 16 到 2047 的字段编号占用两个字节。您可以在协议缓冲区编码中了解更多信息

重用字段编号的后果 {#consequences}

重用字段编号会导致解码线格式消息时出现歧义

protobuf 线格式精简,不提供检测使用一个定义编码而使用另一个定义解码字段的方法

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

字段编号重用的常见原因:

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

指定字段基数 {#field-labels}

消息字段可以是以下之一:

重复字段默认打包 {#use-packed}

在 proto3 中,标量数值类型的 repeated 字段默认使用 packed 编码

您可以在协议缓冲区编码中了解更多关于 packed 编码的信息

消息类型字段始终具有字段存在性 {#field-presence}

在 proto3 中,消息类型字段已具有字段存在性。因此,添加 optional 修饰符不会更改字段的字段存在性

以下代码示例中 Message2Message3 的定义为所有语言生成相同的代码,且在二进制、JSON 和 TextFormat 中的表示没有区别:

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1;
}

message Message3 {
  optional Message1 bar = 1;
}

格式良好的消息 {#well-formed}

术语"格式良好"(well-formed)应用于 protobuf 消息时,指序列化/反序列化的字节。protoc 解析器会验证给定的 proto 定义文件是否可解析

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

添加更多消息类型 {#adding-types}

可以在单个 .proto 文件中定义多个消息类型。如果要定义多个相关消息(例如,如果想定义与 SearchResponse 消息类型对应的回复消息格式),可以将其添加到同一 .proto

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

message SearchResponse {
 ...
}

组合消息会导致臃肿 虽然可以在单个 .proto 文件中定义多种消息类型(如 message、enum 和 service),但当在单个文件中定义大量具有不同依赖关系的消息时,也可能导致依赖膨胀。建议每个 .proto 文件包含尽可能少的消息类型

添加注释 {#adding-comments}

.proto 文件添加注释:

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

  // 需要哪一页?
  int32 page_number = 2;

  // 每页返回的结果数
  int32 results_per_page = 3;
}

删除字段 {#deleting}

如果操作不当,删除字段可能导致严重问题

当不再需要某个字段且客户端代码中所有引用已被删除时,可以从消息中删除字段定义。但是,您必须保留已删除字段的编号。如果不保留字段编号,开发人员将来可能会重用该编号

您还应保留字段名称以允许 JSON 和 TextFormat 编码的消息继续解析

保留字段编号 {#reserved-field-numbers}

如果通过完全删除字段或将其注释掉来更新消息类型,未来开发人员在更新类型时可以重用该数字值。如果稍后加载相同 .proto 的旧实例,可能导致严重问题,包括数据损坏、隐私漏洞等。防止这种情况的一种方法是将已删除条目的数字值(和/或名称,这也可能导致 JSON 序列化问题)指定为 reserved。如果任何未来用户尝试使用这些标识符,协议缓冲区编译器将报错

message Foo {
  reserved 2, 15, 9 to 11;
}

保留字段编号范围是包含的(9 to 11 等同于 9, 10, 11

保留字段名称 {#reserved-field-names}

稍后重用旧字段名称通常是安全的,除非使用 TextProto 或 JSON 编码(字段名称被序列化)。为避免此风险,可以将已删除字段名称添加到 reserved 列表

保留名称仅影响 protoc 编译器行为,不影响运行时行为(一个例外:TextProto 实现在解析时可能丢弃具有保留名称的未知字段(不引发像其他未知字段那样的错误),目前仅 C++ 和 Go 实现如此)。运行时 JSON 解析不受保留名称影响

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注意不能在同一个 reserved 语句中混合字段名称和数字值

.proto 生成什么? {#generated}

.proto 上运行协议缓冲区编译器时,编译器会生成所选语言的代码,这些代码是处理文件中描述的消息类型所需的,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息

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

标量值类型 {#scalar}

标量消息字段可以具有以下类型之一——下表显示了 .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

序列化消息时,您可以在协议缓冲区编码中了解这些类型的编码方式

默认字段值 {#default}

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

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

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

请注意,对于隐式存在的标量字段,一旦消息被解析,就无法判断该字段是显式设置为默认值(例如布尔值设置为 false)还是根本未设置:在定义消息类型时应牢记这一点。例如,如果不希望某些行为在默认情况下也发生,请不要使用布尔值在设置为 false 时触发该行为。另请注意,如果标量消息字段确实设置为其默认值,该值将不会在线序列化。如果浮点或双精度值设置为 +0,则不会序列化,但 -0 被视为不同并将被序列化

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

枚举 {#enum}

定义消息类型时,可能希望其中一个字段仅具有预定义列表中的值。例如,假设您想为每个 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;
}

枚举默认值 {#enum-default}

SearchRequest.corpus 字段的默认值为 CORPUS_UNSPECIFIED,因为这是枚举中定义的第一个值

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

还建议此第一个默认值除"此值未指定"外没有其他语义含义

枚举值别名 {#enum-aliases}

可以通过为不同的枚举常量分配相同的值来定义别名。为此,需要将 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;  // 取消注释此行将导致警告消息
  ENAA_FINISHED = 2;
}

枚举器常量必须在 32 位整数的范围内。由于 enum 值在线上使用变长编码,负值效率低下,因此不推荐使用。您可以在消息定义中定义 enum(如前面的示例所示),也可以在外部定义——这些 enum 可以在 .proto 文件中的任何消息定义中重用。您还可以使用在一个消息中声明的 enum 类型作为另一个消息中字段的类型,语法为 _MessageType_._EnumType_

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

{{% alert title="重要" color="warning" %}}
生成的代码可能受语言特定限制(如一种语言的枚举器数量限制在数千个)。请查看您计划使用的语言的限制
{{% /alert %}}

反序列化期间,无法识别的枚举值将保留在消息中,但消息反序列化时如何表示取决于语言。在支持开放枚举类型(值超出指定符号范围)的语言(如 C++ 和 Go)中,未知枚举值仅存储为其基础整数表示。在具有封闭枚举类型的语言(如 Java)中,使用枚举中的 case 表示无法识别的值,并且可以使用特殊访问器访问基础整数。在任何情况下,如果消息被序列化,无法识别的值仍将随消息一起序列化

{{% alert title="重要" color="warning" %}}
有关枚举应如何工作与目前在不同语言中工作方式的对比信息,请参阅枚举行为
{{% /alert %}}

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

保留值 {#reserved}

如果通过完全删除枚举条目或将其注释掉来更新枚举类型,未来用户可以在更新类型时重用数字值。如果稍后加载相同 .proto 的旧实例,可能导致严重问题(包括数据损坏、隐私漏洞等)。确保不会发生这种情况的一种方法是指定已删除条目的数字值(和/或名称,这也可能导致 JSON 序列化问题)为 reserved。如果任何未来用户尝试使用这些标识符,协议缓冲区编译器将报错。您可以使用 max 关键字指定保留的数字值范围上限

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

注意不能在同一个 reserved 语句中混合字段名称和数字值

使用其他消息类型 {#other}

您可以使用其他消息类型作为字段类型。例如,假设您想在每个 SearchResponse 消息中包含 Result 消息——为此,您可以在同一 .proto 中定义 Result 消息类型,然后在 SearchResponse 中指定类型为 Result 的字段:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

导入定义 {#importing}

在前面的示例中,Result 消息类型与 SearchResponse 定义在同一文件中——如果要用作字段类型的消息类型已在另一个 .proto 文件中定义怎么办?

您可以通过导入来使用其他 .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
// 所有定义已移至此
// old.proto
// 所有客户端正在导入的 proto
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// 您使用 old.proto 和 new.proto 中的定义,但不使用 other.proto

协议编译器在协议编译器命令行使用 -I/--proto_path 标志指定的一组目录中搜索导入的文件。如果未给出标志,则在调用编译器的目录中查找。通常应将 --proto_path 标志设置为项目的根目录,并对所有导入使用完全限定名称

使用 proto2 消息类型 {#proto2}

可以导入 proto2 消息类型并在 proto3 消息中使用它们,反之亦然。但是,proto2 枚举不能直接在 proto3 语法中使用(如果导入的 proto2 消息使用它们则可以)

嵌套类型 {#nested}

您可以在其他消息类型内部定义和使用消息类型,如下例所示——这里 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 {       // 级别 0
  message MiddleAA {  // 级别 1
    message Inner {   // 级别 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // 级别 1
    message Inner {   // 级别 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

更新消息类型 {#updating}

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

{{% alert title="注意" color="note" %}}
如果使用 JSON 或 proto 文本格式存储协议缓冲区消息,您可以在 proto 定义中进行的更改是不同的
{{% /alert %}}

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

未知字段 {#unknowns}

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

Proto3 消息保留未知字段,并在解析和序列化输出中包含它们,这与 proto2 行为匹配

保留未知字段 {#retaining}

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

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

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

Any {#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() 方法:

// 在 Any 中存储任意消息类型
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// 从 Any 读取任意消息
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... 处理 network_error ...
  }
}

Oneof {#oneof}

如果消息中有许多单数字段,并且最多同时设置一个字段,则可以使用 oneof 功能强制执行此行为并节省内存

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

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

oneof 字段的字段编号在封闭消息中必须唯一

使用 Oneof {#using-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-features}

向后兼容性问题 {#backward}

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

标签重用问题 {#reuse}

映射 {#maps}

如果要在数据定义中创建关联映射,Protocol Buffers 提供了方便的快捷语法:

map<key_type, value_type> map_field = N;

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

因此,例如,如果要创建项目映射,其中每个 Project 消息与字符串键关联,可以这样定义:

map<string, Project> projects = 3;

映射特性 {#maps-features}

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

向后兼容性 {#backwards}

映射语法在线上等同于以下内容,因此不支持映射的 Protocol Buffers 实现仍可以处理您的数据:

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

repeated MapFieldEntry map_field = N;

支持映射的任何 Protocol Buffers 实现必须生成和接受可由先前定义接受的数据

包 {#packages}

可以向 .proto 文件添加可选的 package 说明符,以防止协议消息类型之间的名称冲突

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

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

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

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

请注意,即使 package 指令不直接影响生成的代码(例如在 Python 中),仍强烈建议为 .proto 文件指定包,否则可能导致描述符中的命名冲突,并使 proto 对其他语言不可移植

包和名称解析 {#name-resolution}

Protocol Buffer 语言中的类型名称解析类似于 C++:首先搜索最内层作用域,然后搜索次内层,依此类推,每个包被视为其父包的"内部"。前导 '.'(例如 .foo.bar.Baz)表示从最外层作用域开始

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

定义服务 {#services}

如果要在 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 实现。有关我们已知项目的链接列表,请参阅第三方附加组件 wiki 页面

JSON 映射 {#json}

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

选项 {#options}

.proto 文件中的各个声明可以使用多个选项进行注释。选项不会更改声明的整体含义,但可能会影响其在特定上下文中的处理方式。可用选项的完整列表在 /google/protobuf/descriptor.proto 中定义

有些选项是文件级选项,意味着应写在顶级作用域中(不在任何消息、枚举或服务定义内)。有些选项是消息级选项,意味着应写在消息定义内。有些选项是字段级选项,意味着应写在字段定义内。选项也可以写在枚举类型、枚举值、oneof 字段、服务类型和服务方法上;但是,目前这些没有有用的选项

以下是一些最常用的选项:

枚举值选项 {#enum-value-options}

支持枚举值选项。您可以使用 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);

有关如何将自定义选项应用于枚举值和字段,请参阅自定义选项

自定义选项 {#customoptions}

Protocol Buffers 还允许您定义和使用自己的选项。请注意,这是高级功能,大多数人不需要。如果确实认为需要创建自己的选项,请参阅 Proto2 语言指南了解详细信息。请注意,创建自定义选项使用扩展,在 proto3 中仅允许用于自定义选项

选项保留 {#option-retention}

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

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

可以直接在选项上设置保留,如下所示:

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 来覆盖

{{% alert title="注意" color="note" %}}
截至 Protocol Buffers 22.0,对选项保留的支持仍在进行中,仅 C++ 和 Java 受支持。Go 从 1.29.0 开始支持。Python 支持已完成,但尚未包含在版本中
{{% /alert %}}

选项目标 {#option-targets}

字段具有 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;
}

// 正确:此字段允许在文件选项上
option (file_options).file_only_option = "abc";

message MyMessage {
  // 正确:此字段允许在消息和枚举选项上
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // 错误:file_only_option 不能在枚举上设置
  option (enum_options).file_only_option = "xyz";
}

生成类 {#generating}

要生成处理 .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

注意: 文件路径相对于其 proto_path 在给定二进制文件中必须是全局唯一的。例如,如果有 proto/lib1/data.protoproto/lib2/data.proto,则不能将这两个文件与 -I=proto/lib1 -I=proto/lib2 一起使用,因为 import "data.proto" 的含义将不明确。而应使用 -Iproto/,全局名称将为 lib1/data.protolib2/data.proto

如果发布库且其他用户可能直接使用您的消息,应在路径中包含唯一的库名称(他们期望使用的路径下),以避免文件名冲突。如果一个项目中有多个目录,最佳实践是首选设置一个 -I 到项目的顶级目录

文件位置 {#location}

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

位置应与语言无关 {#location-language-agnostic}

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

此规则的例外情况是明确 protos 仅在 Java 上下文中使用(例如用于测试)

支持的平台 {#platforms}

有关信息: