晚上好 世界

晚上好 世界

早安

设计一个面向未来的 API 异常困难。本文档中的建议通过权衡取舍,以支持长期无缺陷的演进。

本文档是对 Proto 最佳实践的补充, 并非 Java/C++/Go 等 API 的规范。

⚠️ 注意
这些准则并非绝对,许多情况都有例外。例如, 若编写性能关键的后端服务,可能需牺牲灵活性或安全性换取速度。 本文旨在帮助您理解权衡取舍,做出适合自身场景的决策。

精准简洁地文档化字段和消息

您的 proto 很可能被不了解原始设计思路的继承者使用。 请以新团队成员或系统知识有限的客户端视角, 为每个字段编写实用文档。

具体示例:

// 反面示例:启用 Foo 的选项
// 正面示例:控制 Foo 功能行为的配置
message FeatureFooConfig {
  // 反面示例:设置功能是否启用
  // 正面示例:必填字段,指示 Foo 功能是否对 account_id 启用。
  // 若 account_id 未设置 FOO_OPTIN Gaia 位,必须为 false。
  optional bool enabled;
}

// 反面示例:Foo 对象
// 正面示例:API 中暴露的 Foo (what/foo) 客户端表示
message Foo {
  // 反面示例:Foo 的标题
  // 正面示例:表示用户提供的 Foo 标题(未经标准化或转义)
  // 示例标题:"Picture of my cat in a box <3 <3 !!!"
  optional string title [(max_length) = 512];
}

// 反面示例:Foo 配置
// 次优方案:若最有用的注释仅是复述名称,不如省略注释
FooConfig foo_config = 3;

用最简练的语言描述每个字段的约束、期望和解释规则。

可使用自定义 proto 注解(如示例中的 max_length), 详见自定义选项。 支持 proto2 和 proto3。

接口文档会随时间增长,冗长将降低清晰度。 当文档确实不清晰时,应修复它, 但需整体审视并追求简洁。

区分传输层与存储层的消息结构

若客户端接口与磁盘存储使用相同的顶层 proto, 将引发隐患。随时间推移,更多二进制文件会依赖您的 API, 使其难以修改。您需要在不影响客户端的情况下自由变更存储格式。 通过分层代码,让模块分别处理客户端 proto、存储 proto 或转换逻辑。

为何? 您可能需要更换底层存储系统, 调整数据归一化/反归一化策略, 或发现部分客户端 proto 适合存入 RAM 而其他部分应落盘。

对于嵌套在请求/响应中的 proto, 分离存储层与传输层的必要性会降低, 这取决于您希望客户端与这些 proto 的耦合程度。

维护转换层虽有成本,但在拥有客户端 且需首次变更存储格式时,其价值会迅速显现。

若试图”需要时再分离”共享 proto, 由于分离成本高且内部字段无处存放, API 将积累客户端不理解或未经授权就依赖的字段。

通过独立的 proto 文件起步, 团队将明确添加内部字段的位置, 避免污染 API。早期可通过自动转换层 (如字节复制或 proto 反射)保持传输层 proto 完全一致。 Proto 注解也可驱动自动转换层。

例外情况

  • 若字段属于通用类型(如 google.typegoogle.protobuf), 可同时用于存储和 API
  • 服务对性能极度敏感时,可用灵活性换取执行速度。 若服务未达到毫秒级延迟的数百万 QPS,通常不属此类例外
  • 同时满足以下条件:
    • 您的服务即是存储系统
    • 系统不基于客户端的结构化数据做决策
    • 系统仅按客户端请求存储、加载或查询数据

注意:若实现日志系统或通用存储的 proto 包装器, 应让客户端消息尽可能透明地进入存储后端, 避免创建依赖枢纽。可考虑使用扩展或 通过 Web 安全编码将二进制序列化数据编码为字符串

支持局部更新或仅追加更新,而非完全替换

避免创建仅接收 FooUpdateFooRequest

若客户端不保留未知字段,其 GetFooResponse 将缺少新字段, 导致往返数据丢失。部分系统不保留未知字段。 Proto2/proto3 实现会保留未知字段,除非显式丢弃。 通常公共 API 应在服务端丢弃未知字段以防止安全攻击 (例如垃圾未知字段可能在服务端未来将其用作新字段时引发故障)。

未明确文档化时,可选字段的处理存在歧义: UpdateFoo 会清空字段吗?当客户端未知该字段时会导致数据丢失。 不修改字段?客户端如何清空字段?两种方案均不理想。

解决方案1:使用更新字段掩码

客户端传递需修改的字段,服务端仅更新掩码指定字段。 掩码结构应与响应 proto 结构镜像: 若 Foo 包含 Bar,则 FooMask 应包含 BarMask

解决方案2:暴露更细粒度的原子操作

例如,用 PromoteEmployeeRequestSetEmployeePayRequestTransferEmployeeRequest 等替代 UpdateEmployeeRequest

定制更新方法比通用更新方法更易监控、审计和防护。 其实现和调用也更简单。但过多定制方法会增加 API 认知负担。

避免在顶层请求/响应中包含原始类型

本规则可规避文档中描述的诸多陷阱。

例如:通过消息包装重复字段, 可区分存储层的未设置状态与特定调用中的未填充状态。

共享的通用请求选项将自然遵循此规则, 读写字段掩码也由此衍生。

顶层 proto 应始终作为可独立扩展的其他消息的容器。

即使当前仅需单个原始类型,将其包装在消息中 也为后续扩展和类型共享提供清晰路径。例如:

message MultiplicationResponse {
  // 反面示例:后续需返回复数时如何处理?
  // 若 AdditionResponse 需返回相同多字段类型?
  optional double result;

  // 正面示例:其他方法可复用此类型,随服务功能扩展而演进
  // (如添加单位、置信区间等)
  optional NumericResult result;
}

message NumericResult {
  optional double real_value;
  optional double complex_value;
  optional UnitType units;
}

例外:若字符串(或字节)实际编码 proto 且仅在服务端构建解析, 可将续传令牌、版本令牌和 ID 作为字符串返回, 但字符串必须是结构化 proto 的编码(遵循下文准则)。

切勿用布尔值表示未来可能扩展的状态

若字段确实仅描述两种状态(永久性而非短期),可使用布尔值。 通常枚举、整型或消息提供的灵活性更值得投入。

例如,返回帖子流时,开发者可能需根据当前 UX 原型 决定是否双栏渲染。即使当前仅需布尔值, 也无法阻止未来引入双行帖、三栏帖或四宫格帖。

message GooglePlusPost {
  // 反面示例:是否跨双栏渲染
  optional bool big_post;

  // 正面示例:客户端渲染提示
  // 客户端应据此决定渲染突出程度,缺省时使用默认渲染
  optional LayoutConfig layout_config;
}

message Photo {
  // 反面示例:是否为 GIF
  optional bool gif;

  // 正面示例:引用照片的文件格式(如 GIF/WebP/PNG)
  optional PhotoType type;
}

谨慎添加枚举状态。若状态引入新维度或隐含多重行为, 几乎必然需要新字段。

避免使用整型字段作为 ID

用 int64 作为对象 ID 很诱人,但应优先选择字符串。

这便于后续变更 ID 空间并减少冲突概率。 2^64 在现代已非足够大。

也可将结构化 ID 编码为字符串,鼓励客户端视其为不透明数据。 此时仍需 proto 支撑字符串,但序列化为字符串字段 (如 Web-safe Base64 编码)可从客户端 API 剥离内部细节。 具体遵循下文准则

message GetFooRequest {
  // 指定获取的 Foo
  optional string foo_id;
}

// 序列化并 websafe-base64 编码到 GetFooRequest.foo_id
message InternalFooRef {
  // 仅设置其一。已迁移的 Foo 使用 spanner_foo_id,
  // 未迁移的使用 classic_foo_id
  optional bytes spanner_foo_id;
  optional int64 classic_foo_id;
}

若自行设计 ID 字符串序列化方案,可能快速引发问题。 因此最好用内部 proto 支撑字符串字段。

勿在字符串中编码需客户端构造/解析的数据

网络传输效率低,增加 proto 消费者工作量, 且令文档读者困惑。客户端还需考虑编码细节: 列表是否逗号分隔?非信任数据是否正确转义?数字是否十进制? 更好的方案是让客户端发送实际消息或原始类型, 网络传输更紧凑,客户端更清晰。

当服务拥有多语言客户端时尤其严重, 每个客户端需选择正确解析器/构建器(或更糟——自行编写)。

通用原则:选择正确的原始类型。 参见 Protocol Buffer 语言指南中的标量类型表。

勿在前端 proto 中返回 HTML

对 JavaScript 客户端,在 API 字段中返回 HTML 或 JSON 很诱人, 但这易导致 API 与特定 UI 绑定。三大风险:

  • 非 Web 客户端可能解析您的 HTML/JSON 获取数据, 若您变更格式会导致脆弱性,解析不当则引发漏洞
  • 若 HTML 未净化返回,Web 客户端面临 XSS 攻击风险
  • 返回的标签和类依赖特定样式表和 DOM 结构。 随版本迭代结构变更,可能引发版本偏差问题: 旧版客户端无法正确渲染新版服务返回的 HTML

除初始页面加载外, 通常应在客户端返回数据并用客户端模板构建 HTML。

通过 Web 安全编码将二进制序列化数据编码为字符串

若需在客户端可见字段编码不透明数据 (续传令牌、序列化 ID、版本信息等), 需文档化要求客户端视其为不透明数据。 始终使用二进制 proto 序列化,勿用文本格式或自定义方案。

定义内部 proto 保存字段(即使仅需一个字段), 序列化为字节后,用 Web-safe base64 编码到字符串字段。

罕见例外:若定制格式的紧凑性收益显著,可替代序列化方案。

勿包含客户端无需的字段

暴露给客户端的 API 应仅描述系统交互方式。 添加其他内容会增加理解成本。

过去常在响应 proto 返回调试数据,现有更佳方案: RPC 响应扩展(或称”旁路通道”)可分离客户端接口与调试接口。

类似地,返回实验名称曾是日志便利手段 ——隐含约定是客户端在后续操作中回传这些实验。 当前推荐方案是在分析管道中进行日志关联。

例外情况: 若需实时持续分析机器资源紧张, 日志关联成本过高时可预先反规范化日志数据。 如需日志数据往返传递,可作为不透明数据发送给客户端, 并文档化请求/响应字段。

注意:若需在每个请求中返回或往返隐藏数据, 您掩盖了服务的真实使用成本,这同样不利。

定义分页 API 时务必包含续传令牌

message FooQuery {
  // 反面示例:若两次查询间数据变更,以下策略均可能导致结果遗漏
  // 在最终一致性系统(如 Bigtable 存储)中,新旧数据交错出现很常见
  // 且偏移量/分页方案均假设排序规则,丧失灵活性
  optional int64 max_timestamp_ms;
  optional int32 result_offset;
  optional int32 page_number;
  optional int32 page_size;

  // 正面示例:灵活性!在 FooQueryResponse 中返回,
  // 客户端下次查询时回传
  optional string next_page_token;
}

分页 API 最佳实践是使用不透明续传令牌(称 next_page_token), 由内部 proto 支撑(序列化后执行 WebSafeBase64Escape (C++) 或 BaseEncoding.base64Url().encode (Java))。内部 proto 可包含多字段, 关键是通过灵活性为客户端提供结果稳定性。

勿忘验证 proto 字段的不可信输入 (参见准则)。

message InternalPaginationToken {
  // 追踪已见 ID:以续传令牌增大为代价实现完美召回
  repeated FooRef seen_ids;

  // 类似 seen_ids 策略,但用布隆过滤器节省字节(牺牲精度)
  optional bytes bloom_filter;

  // 合理初版方案。嵌入续传令牌可在不影响客户端的情况下变更
  optional int64 max_timestamp_ms;
}
message Foo {
  // 反面示例:Foo 的价格和货币类型
  optional int price;
  optional CurrencyType currency;

  // 更佳方案:封装 Foo 的价格和货币类型
  optional CurrencyAmount price;
}

仅高内聚字段应嵌套。若字段确实相关, 在服务内部传递时会更便捷。例如:

CurrencyAmount calculateLocalTax(CurrencyAmount price, Location where)

若变更引入一个字段,但该字段后续可能有相关字段, 应预先放入独立消息以避免:

message Foo {
  // 已弃用!使用 currency_amount
  optional int price [deprecated = true];

  // Foo 的价格和货币类型
  optional google.type.Money currency_amount;
}

嵌套消息的问题是:CurrencyAmount 可能在 API 其他位置复用, 但 Foo.CurrencyAmount 可能不会。最坏情况是复用 Foo.CurrencyAmount, 但 Foo 专属字段泄漏其中。

虽然松耦合 是系统开发的通用准则,但设计 .proto 文件时未必适用。 若两个信息单元高度相关(通过嵌套),可能有意义。 例如,若创建一组当前较通用的字段, 但预期未来添加专属字段,嵌套消息可阻止他人 在其他 .proto 文件中引用该消息。

message Photo {
  // 反面示例:PhotoMetadata 可能在 Photo 范围外复用
  // 因此不嵌套更易访问是优选
  message PhotoMetadata {
    optional int32 width = 1;
    optional int32 height = 2;
  }
  optional PhotoMetadata metadata = 1;
}

message FooConfiguration {
  // 正面示例:在 FooConfiguration 外复用 Rule
  // 会与无关组件紧耦合,嵌套可避免此问题
  message Rule {
    optional float multiplier = 1;
  }
  repeated Rule rules = 1;
}

在读取请求中包含字段读取掩码

// 推荐方案:使用 google.protobuf.FieldMask

// 替代方案一:
message FooReadMask {
  optional bool return_field1;
  optional bool return_field2;
}

// 替代方案二:
message BarReadMask {
  // 需返回的 Bar 字段标签号
  repeated int32 fields_to_return;
}

若使用推荐的 google.protobuf.FieldMask, 可用 FieldMaskUtil (Java/C++) 库自动过滤 proto。

读取掩码为客户端设定期望,控制需返回的数据量, 并让后端仅获取必要数据。

可接受替代方案是始终填充所有字段(即隐式全 true 读取掩码)。 但随着 proto 增长,成本会上升。

最差模式是存在未声明的隐式读取掩码, 且根据填充方法不同而变化。此反模式会导致 通过响应 proto 构建本地缓存的客户端出现数据丢失。

包含版本字段以实现一致性读取

当客户端写入后立即读取同一对象时, 期望得到所写内容(即使底层存储系统不保证此行为)。

服务端读取本地值后,若本地 version_info 低于预期, 将从远端副本读取最新值。通常 version_info 是 字符串编码的 proto, 包含数据中心和提交时间戳。

即使由一致性存储支撑的系统, 也常需令牌触发高成本的一致性读取路径, 而非每次读取均承担此开销。

相同数据类型的 RPC 使用一致的请求选项

反面模式示例:某服务中每个返回相同数据类型的 RPC, 却有独立选项指定最大评论数、支持的嵌入类型列表等。

临时方案会增加客户端填写请求的复杂性, 及服务端转换 N 个选项为通用内部选项的复杂性。 现实中的大量 bug 可追溯至此。

应创建独立的消息容纳请求选项, 并包含在每个顶层请求消息中。改进方案:

message FooRequestOptions {
  // 字段级读取掩码。仅返回请求的字段。
  // 客户端应仅请求所需字段以帮助后端优化
  optional FooReadMask read_mask;

  // 响应中每个 Foo 返回的最大评论数(垃圾评论不计入)。
  // 默认不返回评论
  optional int max_comments_to_return;

  // 包含非支持类型嵌入的 Foo,将降级转换为此列表中的类型。
  // 未指定列表时不返回嵌入内容。若无法降级转换,则不返回嵌入。
  // 强烈建议客户端始终包含 EmbedTypes.proto 中的 THING_V2 类型
  repeated EmbedType embed_supported_types_list;
}

message GetFooRequest {
  // 读取的 Foo。若查看者无权访问或 Foo 已删除,响应为空但成功
  optional string foo_id;

  // 客户端必须包含此字段。若 FooRequestOptions 为空,
  // 服务端返回 INVALID_ARGUMENT
  optional FooRequestOptions params;
}

message ListFooRequest {
  // 返回的 Foo。搜索召回率 100%,但子句越多性能影响越大
  optional FooQuery query;

  // 客户端必须包含此字段。若 FooRequestOptions 为空,
  // 服务端返回 INVALID_ARGUMENT
  optional FooRequestOptions params;
}

批处理/多阶段请求

尽可能保证操作的原子性,更重要的是保证 幂等性。部分失败后的重试不应破坏/重复数据。

有时出于性能需单个 RPC 封装多个操作。 部分失败时如何处理?若部分成功部分失败, 最佳方案是让客户端知晓两者详情。

通常建议将 RPC 设为失败状态, 在 RPC 状态 proto 中返回成功和失败的详情。 理想情况是:未感知部分失败的客户端仍能正确处理, 感知的客户端可获取额外价值。

创建返回/操作小数据块的方法,客户端通过批处理组合 UI

通过单次往返查询多个细粒度数据的能力, 允许客户端组合所需内容,无需服务端变更即可支持更广的 UX 范围。

这对前端和中层服务器最相关。

许多服务提供自有批处理 API。

移动端/Web 需避免串行往返时,应创建一次性 RPC

Web/移动端客户端需进行两次有数据依赖的查询时, 当前最佳实践是创建新 RPC 避免客户端额外往返。

对移动端,几乎总值得通过捆绑两个服务方法为单一新方法 节省往返开销。对服务间调用,情况可能不同, 取决于服务性能敏感度和新方法的认知开销。

重复字段使用消息而非标量或枚举

常见演进场景是单个重复字段需变为多个关联重复字段。 若以原始类型起步,选项有限:要么创建并行重复字段, 要么定义含值容器的新重复字段并迁移客户端。

若以重复消息起步,演进将变得简单。

// 描述照片增强类型
enum EnhancementType {
  ENHANCEMENT_TYPE_UNSPECIFIED;
  RED_EYE_REDUCTION;
  SKIN_SOFTENING;
}

message PhotoEnhancement {
  optional EnhancementType type;
}

message PhotoEnhancementReply {
  // 正面示例:PhotoEnhancement 可扩展描述需更多字段的增强类型
  repeated PhotoEnhancement enhancements;

  // 反面示例:若需返回增强相关参数,
  // 需引入并行数组(糟糕)或弃用此字段改用重复消息
  repeated EnhancementType enhancement_types;
}

设想需求:“需区分用户执行的增强与系统自动应用的增强”。

PhotoEnhancementReply 中是标量或枚举,支持此需求将更困难。

此原则同样适用于 map。若 map 值已是消息类型, 添加字段远比从 map<string, string> 迁移到 map<string, MyProto> 简单。

例外: 延迟敏感型应用会发现原始类型的并行数组比消息数组 构建/删除更快。若使用 packed=true(省略字段标签), 网络传输也更小。Proto3 中自动打包。 分配固定数量数组比分配 N 个消息开销更小。

使用 Proto Map

Proto3 引入 Map 类型前, 服务常使用临时 KVPair 消息暴露数据。当客户端需要深层结构时, 最终需设计需解析的键/值(参见准则)。

因此,对值使用(可扩展的)消息类型是基础改进。

Map 已向后移植到 proto2,故使用 map<标量, **消息**> 优于自定义 KVPair1

若需表示结构未知的任意数据, 请使用 google.protobuf.Any

优先保证幂等性

上层客户端可能包含重试逻辑。若重试的是变更操作, 用户可能遭遇意外:重复评论、构建请求、编辑等对任何人都不利。

避免重复写入的简单方案是允许客户端指定请求 ID(如内容哈希或 UUID), 服务端依此去重。

谨慎命名服务并确保全局唯一性

服务名称(即 .proto 文件中 service 关键字后的部分) 用途远超生成服务类名,使其重要性超乎预期。

棘手之处在于这些工具隐含假设服务名称在网络中唯一。 更糟的是它们使用非限定服务名(如 MyService), 而非限定名(如 my_package.MyService)。

因此,即使服务定义在特定包内, 也应采取措施防止服务名冲突。 例如 Watcher 可能引发问题, MyProjectWatcher 更佳。

限制请求/响应大小

请求/响应大小应有上限。建议约 8 MiB, 2 GiB 是多数 proto 实现的硬限制。 许多存储系统对消息大小有限制。

此外,无界消息会:

  • 膨胀客户端和服务端
  • 导致高且不可预测的延迟
  • 依赖长连接降低弹性

限制消息大小的方案: - 定义返回有界消息的 RPC,每次调用逻辑独立 - 定义操作单个对象的 RPC,而非操作客户端指定的无界列表 - 避免在字符串、字节或重复字段编码无界数据 - 定义长时运行操作。将结果存入支持可扩展并发读取的存储系统 - 使用分页 API(参见准则) - 使用流式 RPC

若开发 UI,另见准则

谨慎传播状态码

RPC 服务应在边界检查错误,并向调用方返回有意义的状态错误。

以简单示例说明:

假设客户端调用无参数的 ProductService.GetProducts。 执行过程中,ProductService 获取所有产品, 并为每个产品调用 LocaleService.LocaliseNutritionFacts

digraph toy_example {
  node [style=filled]
  client [label="Client"];
  product [label="ProductService"];
  locale [label="LocaleService"];
  client -> product [label="GetProducts"]
  product -> locale [label="LocaliseNutritionFacts"]
}

ProductService 实现错误,可能向 LocaleService 发送错误参数导致 INVALID_ARGUMENT

ProductService 草率返回错误,客户端将收到 INVALID_ARGUMENT(状态码跨 RPC 传播)。 但客户端未向 ProductService.GetProducts 传递任何参数! 该错误比无用更糟:将引发严重困惑!

正确做法:ProductService 应在 RPC 边界检查错误 (即其实现的 RPC 处理程序)。若自身收到无效参数, 应返回 INVALID_ARGUMENT;若下游收到无效参数, 应先将 INVALID_ARGUMENT 转为 INTERNAL 再返回。

草率传播状态错误将导致调试成本高昂的困惑, 甚至因服务转发客户端错误却不触发警报, 引发不可见的中断。

通用规则:在 RPC 边界仔细检查错误, 向调用方返回带合适状态码的有意义错误。 为准确传达意图,每个 RPC 方法应文档化其返回的错误码及场景。 方法实现应符合文档约定的 API 契约。

为每个方法创建专属 Proto

为每个 RPC 方法创建唯一的请求/响应 proto。 后期发现需变更顶层请求/响应时成本高昂。 包括”空”响应:应创建专属空响应 proto, 而非复用知名 Empty 类型

消息复用

复用消息时,创建共享”领域”消息类型供多个请求/响应 proto 包含。 应用逻辑应基于这些类型而非请求/响应类型编写。

这使您能独立演进方法请求/响应类型, 同时共享逻辑子单元的代码。

附录

返回重复字段

当重复字段为空时,客户端无法区分是服务端未填充, 还是底层数据确实为空(即重复字段无 hasFoo 方法)。

用消息包装重复字段是获得 hasFoo 方法的简单方案:

message FooList {
  repeated Foo foos;
}

更系统的方案是使用字段读取掩码。 若字段被请求,空列表表示无数据; 若未请求,客户端应忽略响应中的该字段。

更新重复字段

最差方案是强制客户端提供完整替换列表。 此方案风险包括:不保留未知字段的客户端导致数据丢失; 并发写入导致数据丢失;即使无此问题, 客户端也需仔细阅读文档才能理解服务端解释逻辑 (空字段表示不更新还是清空?)。

解决方案1:使用允许客户端替换、删除或插入元素的重复更新掩码, 无需在写入时提供整个数组。

解决方案2:在请求 proto 中创建独立的追加、替换、删除数组。

解决方案3:仅允许追加或清空。 可通过消息包装重复字段实现: 若存在但为空的消息表示清空,否则重复元素表示追加。

重复字段的顺序无关性

尽量避免顺序依赖,这是额外的脆弱层。 最恶劣的顺序依赖是并行数组, 这会增加客户端解读结果的难度, 并使其在服务内部传递时变得不自然。

message BatchEquationSolverResponse {
  // 反面示例:解值按请求方程顺序返回
  repeated double solved_values;
  // (通常)反面示例:solved_values 的并行数组
  repeated double solved_complex_values;
}

// 正面示例:独立消息可扩展更多字段并被其他方法复用。
  // 请求与响应间无顺序依赖,多重复字段间无顺序依赖
message BatchEquationSolverResponse {
  // 已弃用,2014 年 Q2 前响应将继续填充此字段,
  // 之后客户端必须改用下方的 solutions 字段
  repeated double solved_values [deprecated = true];

  // 正面示例:请求中的每个方程有唯一 ID,
  // 包含在下方的 EquationSolution 中以便与解关联。
  // 方程并行求解,解产生后加入此数组
  repeated EquationSolution solutions;
}

因 Proto 进入移动构建导致特性泄露

Android/iOS 运行时均支持反射,为此会将字段和消息的原始名称 以字符串形式嵌入应用二进制文件(APK/IPA)。

message Foo {
  // 此字段将在 Android/iOS 泄露 Google Teleport 项目存在
  optional FeatureStatus google_teleport_enabled;
}

缓解策略:

  • Android 使用 ProGuard 混淆(截至 2014 年 Q3)。 iOS 无可混淆选项:桌面端对 IPA 执行 strings 命令 将暴露 proto 字段名(参见 iOS Chrome 拆解
  • 严格筛选发送至移动客户端的字段
  • 若在可接受时限内无法堵漏,需征得特性负责人同意承担风险

绝不可以此为借口用代号混淆字段含义。 要么堵漏,要么征得同意承担风险。

性能优化

某些情况下可牺牲类型安全或清晰度换取性能。 例如,含数百个字段(特别是消息类型字段)的 proto 解析速度慢于字段少的 proto。 深度嵌套的消息仅因内存管理就会降低反序列化速度。 团队曾用以下技术加速反序列化:

  • 创建精简的并行 proto,仅声明部分标签。 无需所有字段时用于解析。 添加测试确保标签号随精简 proto 产生编号”空洞”而保持匹配
  • [lazy=true] 标注字段
  • 将字段声明为字节类型并文档化其类型。 需解析字段的客户端可手动操作。 此方案风险在于无法防止错误类型的消息放入字节字段。 若 proto 会写入日志,切勿使用此方案, 因其阻碍 PII 审查或策略/隐私擦除。

本文翻译自:2025-7-17 API Best Practices | Protocol Buffers Documentation


  1. map<k,v> 字段的 proto 的陷阱:勿在 MapReduce 中将其作为归约键。 Proto3 map 项的传输格式和迭代顺序未指定,导致不一致的分片结果。↩︎


不使用 all ways gpu 的 5. Set Compositor Specific Variables (Method 3),手动 配置全部应用程序使用 GPU 渲染,感觉上 BUG 少、更流畅:

2025-07-09T10:33:08.png

补充

之前把 all ways gpu 的方法3和 手动配置应用程序使用 GPU 渲染 方法组合使用,导致全屏时莫名奇妙地崩溃

orreo@VC200 ~> org.gnome.gitlab.somas.Apostrophe
Adding pride CSS class flag-disability

<点全屏>
(apostrophe:2): Gdk-WARNING **: 11:01:24.373: vkAcquireNextImageKHR(): A surface has changed in such a way that it is no longer compatible with the swapchain. (VK_ERROR_OUT_OF_DATE_KHR) (-1000001004)
<卡死>

大部分 Wayland 原生应用点全屏都卡死崩溃,XWayland点全屏时界面被冻结

有的时候有问题,有的时候就完全正常。

换到本文方法(关闭 all ways gpu)后问题解决。


本文翻译自 ProtoJSON Format | Protocol Buffers Documentation

Protobuf 支持规范的 JSON 编码格式,便于与不支持标准 Protobuf 二进制有线格式的系统共享数据。

ProtoJSON 格式不如 Protobuf 有线格式高效。转换器在编解码消息时会消耗更多 CPU 资源,且(除少数情况外)编码后的消息占用空间更大。此外,ProtoJSON 格式会将字段名和枚举值名写入编码消息,导致后续重命名这些标识符变得极其困难。删除字段更是破坏性变更,会引发解析错误。简言之,Google 坚持使用标准有线格式而非 ProtoJSON 格式有诸多充分理由。

下文表格按类型详细描述了编码规则。

解析与生成规则

将 JSON 编码数据解析为 Protocol Buffer 时:

  • 若值缺失或为 null,将被解释为对应字段的默认值
  • 单数字段允许多个值(通过重复键或等效 JSON 键),保留最后一个值(与二进制格式解析行为一致)
  • ⚠️ 注意:非所有解析器实现都符合此规范,部分实现可能拒绝重复键

从 Protocol Buffer 生成 JSON 编码数据时:

  • 默认值字段:若字段不支持存在性检测,默认省略该字段(实现方可提供选项强制输出)
  • 显式设置字段:支持存在性检测的字段(如带 optional 关键字的 proto3 字段、任意版本的 message 类型字段)始终输出,即使值为默认值
  • Proto3 隐式存在标量字段:仅当值非类型默认值时输出
  • 数值处理:若解析值超出目标类型范围,将按 C++ 强制转换规则处理(如 64 位整数转为 int32 时将被截断)

JSON 数据表示对照表

Protobuf 类型 JSON 类型 JSON 示例 说明
message object {"fooBar": v, "g": null, ...} • 字段名转换为小驼峰命名(lowerCamelCase)作为 JSON 键
• 若指定 json_name 字段选项,则使用该值作为键
• 解析器同时接受小驼峰名(或 json_name 值)和原始字段名
null 会被解释为字段默认值
• ⚠️ json_name 禁止设为 null(详见严格校验说明
enum string "FOO_BAR" • 使用 proto 定义的枚举值名
• 解析器同时接受枚举名和整数值
map<K,V> object {"k": v, ...} 所有键均转换为字符串
repeated V array [v, ...] null 等价于空列表 []
bool boolean true, false -
string string "Hello World!" -
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" • 输出:标准 base64 编码(含填充)
• 输入:接受标准/URL安全 base64(含/不含填充)
int32, fixed32, uint32 number 1, -10, 0 • 接受数字或字符串
• 空字符串无效
int64, fixed64, uint64 string "1", "-10" • 接受数字或字符串
• 空字符串无效
float, double number/string 1.1, "NaN", "-Infinity" • 接受数字或特殊字符串("NaN"/"Infinity"/"-Infinity")
• 支持科学计数法
• 空字符串无效
Any object {"@type": "url", "value": yyy} 含特殊 JSON 映射的值按固定结构转换,其他值转换后插入 "@type" 字段标识类型
Timestamp string "1972-01-01T10:00:20.021Z" • 输出:RFC 3339 格式(Z 标准化,含 0/3/6/9 位小数)
• 输入:接受非 "Z" 时区偏移
Duration string "1.000340012s" • 输出:含 0/3/6/9 位小数 + "s" 后缀
• 输入:接受任意精度纳秒值 + "s" 后缀
Struct object { ... } 任意 JSON 对象(参见 struct.proto
Wrapper types 对应基础类型 2, "foo", true, null 包装类型使用原始类型表示规则,但允许保留 null
FieldMask string "f.fooBar,h" 参见 field_mask.proto
ListValue array [foo, bar, ...] -
Value any 任意值 任意 JSON 值(详见 google.protobuf.Value
NullValue null null JSON null
Empty object {} 空 JSON 对象

JSON 选项 {#json-options}

符合规范的 Protobuf JSON 实现可提供以下选项:

始终输出无存在性字段

  • 默认行为:省略不支持存在性检测且为默认值的字段(如 proto3 隐式存在字段的 0 值、空字符串、空列表/映射)
  • 选项:可覆盖默认行为,强制输出含默认值的字段
  • ⚠️ 现状:截至 v25.x,C++/Java/Python 实现存在不一致性(影响 proto2 optional 但不影响 proto3 optional),后续版本将修复

忽略未知字段

  • 默认行为:拒绝未知字段
  • 选项:解析时忽略未知字段

使用 proto 字段名替代小驼峰命名

  • 默认行为:字段名转为小驼峰命名作为 JSON 键
  • 选项:直接使用 proto 字段名作为 JSON 键
  • ⚠️ 要求:解析器必须同时接受小驼峰名和原始字段名

枚举值输出为整数

  • 默认行为:输出枚举值名称
  • 选项:输出枚举值对应的数字

本文翻译自 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;
}
  • 文件第一行指定您正在使用 protobuf 语言规范的 proto3 版本

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

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

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

分配字段编号 {#assigning}

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

  • 给定数字在该消息的所有字段中必须唯一
  • 字段编号 19,00019,999 保留给 Protocol Buffers 实现使用。如果您在消息中使用这些保留字段号,协议缓冲区编译器将报错
  • 不能使用任何先前保留的字段编号或已分配给扩展的字段编号

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

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

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

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

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

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

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

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

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

  • 重新编号字段(有时为实现更美观的字段编号顺序)。重新编号实际上会删除并重新添加涉及的所有字段,导致不兼容的线格式更改
  • 删除字段但未保留编号以防止未来重用

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

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

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

  • 单数(Singular):
    在 proto3 中,单数字段有两种类型:

    • optional:(推荐)optional 字段处于两种可能状态之一:

      • 字段已设置,包含显式设置或从线解析的值。它将序列化到线
      • 字段未设置,将返回默认值。不会序列化到线
        您可以检查值是否被显式设置
        为获得与 protobuf editions 和 proto2 的最大兼容性,推荐使用 optional 而非隐式字段
    • 隐式:(不推荐)隐式字段没有显式基数标签,行为如下:

      • 如果字段是消息类型,其行为类似于 optional 字段
      • 如果字段非消息类型,有两种状态:

        • 字段设置为显式设置或从线解析的非默认(非零)值。它将序列化到线
        • 字段设置为默认(零)值。不会序列化到线。实际上,您无法确定默认(零)值是设置、从线解析还是根本未提供。有关此主题的更多信息,请参阅字段存在性
  • repeated:此字段类型在格式良好的消息中可以重复零次或多次。重复值的顺序将被保留
  • map:这是键/值对字段类型。有关此字段类型的更多信息,请参阅映射

重复字段默认打包 {#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 文件添加注释:

  • 建议在 .proto 代码元素之前的行使用 C/C++/Java 行尾风格注释 '//'
  • 也接受 C 风格内联/多行注释 /* ... */

    • 使用多行注释时,建议使用 '*' 作为边距行
/**
 * 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 上运行协议缓冲区编译器时,编译器会生成所选语言的代码,这些代码是处理文件中描述的消息类型所需的,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息

  • 对于 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 参考

标量值类型 {#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
  • 对于数值类型,默认值为零
  • 对于消息字段,未设置字段。其确切值取决于语言。详情请参阅生成代码指南
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为 0。参见枚举默认值

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

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

请注意,对于隐式存在的标量字段,一旦消息被解析,就无法判断该字段是显式设置为默认值(例如布尔值设置为 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。这是因为:

  • 必须有一个零值,以便我们可以使用 0 作为数值默认值
  • 零值需要是第一个元素,以与 proto2 语义兼容(其中第一个枚举值是默认值,除非显式指定了不同的值)

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

枚举值别名 {#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 最佳实践和以下规则:

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

未知字段 {#unknowns}

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

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

保留未知字段 {#retaining}

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

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

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

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

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}

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

    SampleMessage message;
    message.set_name("name");
    CHECK_EQ(message.name(), "name");
    // 调用 mutable_sub_message() 将清除 name 字段并将 sub_message 设置为 SubMessage 的新实例(未设置任何字段)
    message.mutable_sub_message();
    CHECK(message.name().empty());
  • 如果解析器在线上遇到同一 oneof 的多个成员,则仅使用解析消息中看到的最后一个成员。从字节开头开始在线解析数据时,评估下一个值并应用以下解析规则:

    • 首先,检查是否当前设置了同一 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");      // 将删除 sub_message
    sub_message->set_...            // 在此处崩溃
  • 同样在 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");

向后兼容性问题 {#backward}

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

标签重用问题 {#reuse}

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

映射 {#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}

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

生成的映射 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;
  ...
}

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

  • 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 对其他语言不可移植

包和名称解析 {#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 字段、服务类型和服务方法上;但是,目前这些没有有用的选项

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

  • 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: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_servicesjava_generic_servicespy_generic_services(文件选项):通用服务已弃用。 Protocol Buffer 编译器是否应根据 服务定义在 C++、Java 和 Python 中生成抽象服务代码。出于遗留原因,这些默认为 true。但是,自 2.3.0 版(2010 年 1 月)起,RPC 实现提供代码生成器插件为每个系统生成更具体的代码,而不是依赖"抽象"服务,这被认为是更可取的

    // 此文件依赖插件生成服务代码
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
  • cc_enable_arenas(文件选项):为 C++ 生成的代码启用竞技场分配
  • objc_class_prefix(文件选项):设置 Objective-C 类前缀,该前缀将添加到此 .proto 生成的所有 Objective-C 类和枚举。无默认值。应使用 3-5 个大写字符作为前缀(如 Apple 推荐)。请注意,所有 2 个字母的前缀由 Apple 保留
  • packed(字段选项):在基本数值类型的重复字段上默认为 true,导致使用更紧凑的编码。要使用非打包线格式,可设置为 false。这提供了与 2.3.0 版之前解析器的兼容性(很少需要),如下例所示:

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

    int32 old_field = 6 [deprecated = true];

枚举值选项 {#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
  • IMPORT_PATH 指定解析 import 指令时查找 .proto 文件的目录。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项指定多个导入目录。-I=_IMPORT_PATH_ 可用作 --proto_path 的简短形式

注意: 文件路径相对于其 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 到项目的顶级目录

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

    作为额外便利,如果 DST_DIR.zip.jar 结尾,编译器将输出写入具有给定名称的单个 ZIP 格式存档文件。.jar 输出还将根据需要提供清单文件(如 Java JAR 规范所要求)。请注意,如果输出存档已存在,将被覆盖

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

文件位置 {#location}

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

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

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

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

支持的平台 {#platforms}

有关信息:


VS Code 的 Code Lens 是一项增强代码编辑体验的功能,它会在代码中直接嵌入动态的、可交互的上下文信息(如引用、测试状态、Git 变更等),帮助开发者快速获取关键信息而无需跳转界面。以下是详细说明:


核心功能

  1. 引用显示(References)

    • 在函数、类或变量上方显示被引用的次数(例如:• 12 references),点击后可查看所有引用位置。
    • 作用:快速了解代码被哪些部分依赖。
  2. 测试状态(Test)

    • 如果项目有测试框架(如 Jest、Mocha),Code Lens 会显示测试用例的运行状态(例如:▶ Run Test | ✓ Passed)。
    • 作用:直接运行或调试测试,无需切换文件。
  3. Git 历史(GitLens 扩展)

    • 通过安装 GitLens 扩展,Code Lens 会显示代码行的最近修改作者、时间和提交信息。
    • 作用:追踪代码变更历史,辅助协作。
  4. 实现与继承(Implementations)

    • 对于接口或抽象类,显示实现该接口的子类数量(例如:• 3 implementations)。

如何启用/关闭

  • 默认状态:Code Lens 默认开启,但部分功能(如 Git 历史)需安装扩展。
  • 手动配置
    在设置中搜索 editor.codeLens

    • "editor.codeLens": true/false(全局开关)。
    • 针对语言单独配置(如 "[python]": { "editor.codeLens": false })。

优点

  • 减少上下文切换:直接查看关键信息,无需跳转到其他面板。
  • 提升效率:快速运行测试、查看引用或 Git 记录。
  • 可定制性:通过扩展增强功能(如 GitLens、Test Adapters)。

相关拓展

  • 插件市场搜索 Lens

2025-06-08T13:26:07.png

Error Lens

2025-06-08T13:26:40.png

配置说明

Version Lens

2025-06-08T13:31:14.png

Hover Lens

2025-06-08T13:30:12.png