Proto 最佳实践

分享经过审查的 Protocol Buffers 编写最佳实践。

客户端和服务器永远不会在完全相同的时间更新 - 即使您尝试同时更新它们也是如此。其中一个或另一个可能会回滚。不要假设您可以进行破坏性更改,并且没问题,因为客户端和服务器是同步的。

不要 重用标签号

永远不要重用标签号。它会搞乱反序列化。即使您认为没有人使用该字段,也不要重用标签号。如果更改曾经生效,则在某处的日志中可能存在 Proto 的序列化版本。或者在另一个服务器中可能存在旧代码,这些代码会中断。

应该 为已删除的字段保留标签号

当您删除不再使用的字段时,请保留其标签号,以便将来没有人意外地重用它。只需 reserved 2, 3; 就足够了。不需要类型(让您修剪依赖项!)。您还可以保留名称以避免回收现在已删除的字段名称:reserved "foo", "bar";

应该 为已删除的枚举值保留数字

当您删除不再使用的枚举值时,请保留其编号,以便将来没有人意外地重用它。只需 reserved 2, 3; 就足够了。您还可以保留名称以避免回收现在已删除的值名称:reserved "FOO", "BAR";

应该 将新的枚举别名放在最后

当您添加新的枚举别名时,请将新名称放在最后,以便为服务提供时间来接收它。

要安全地删除原始名称(如果它正在用于互换,这是不应该的),您必须执行以下操作

  • 在旧名称下方添加新名称并弃用旧名称(序列化程序将继续使用旧名称)

  • 在每个解析器都推出架构后,交换两个名称的顺序(序列化程序将开始使用新名称,解析器接受两者)

  • 在每个序列化程序都具有该版本的架构后,您可以删除已弃用的名称。

注意: 虽然理论上客户端不应将旧名称用于互换,但遵循上述步骤仍然是礼貌的,特别是对于广泛使用的枚举名称。

不要 更改字段的类型

几乎永远不要更改字段的类型;它会像重用标签号一样搞乱反序列化。protobuf 文档概述了少数可以接受的情况(例如,在 int32uint32int64bool 之间切换)。但是,更改字段的消息类型将会中断,除非新消息是旧消息的超集。

不要 添加必需字段

永远不要添加必需字段,而是添加 // required 以记录 API 合约。必需字段被认为是有害的,以至于它们已从 proto3 中完全删除。使所有字段可选或重复。您永远不知道消息类型将持续多久,也不知道是否有人会在四年后被迫用空字符串或零填充您的必需字段,那时它在逻辑上不再是必需的,但 Proto 仍然这样说。

对于 proto3,没有 required 字段,因此此建议不适用。

不要 创建包含大量字段的消息

不要创建具有“大量”(想想:数百个)字段的消息。在 C++ 中,每个字段大约向内存中对象大小添加 65 位,无论是否填充(指针为 8 字节,如果字段声明为可选,则在位字段中再添加一位,用于跟踪字段是否已设置)。当您的 Proto 增长过大时,生成的代码甚至可能无法编译(例如,在 Java 中,方法的长度存在硬性限制)。

应该 在枚举中包含未指定的 Value

枚举应包含默认的 FOO_UNSPECIFIED 值,作为声明中的第一个值 。当新值添加到 proto2 枚举时,旧客户端会将该字段视为未设置,并且 getter 将返回默认值或第一个声明的值(如果不存在默认值) 。为了与 proto 枚举保持一致的行为,第一个声明的枚举值应为默认的 FOO_UNSPECIFIED 值,并且应使用标签 0。将此默认值声明为语义上有意义的值可能很诱人,但作为一般规则,请不要这样做,以帮助您的协议随着时间的推移以及新枚举值的添加而发展。容器消息下声明的所有枚举值都在同一个 C++ 命名空间中,因此请在未指定的值前加上枚举的名称,以避免编译错误。如果您永远不需要跨语言常量,则 int32 将保留未知值并生成更少的代码。请注意,proto 枚举要求第一个值为零,并且可以往返(反序列化、序列化)未知枚举值。

不要 将 C/C++ 宏常量用于枚举值

使用 C++ 语言已定义的单词 - 特别是在其标头(例如 math.h)中,如果其中一个标头的 #include 语句出现在 .proto.h 的语句之前,则可能会导致编译错误。避免使用宏常量(例如“NULL”、“NAN”和“DOMAIN”)作为枚举值。

应该 使用众所周知的类型和通用类型

强烈建议使用以下通用、共享类型。例如,当已经存在完全合适的通用类型时,请勿在代码中使用 int32 timestamp_seconds_since_epochint64 timeout_millis

  • duration 是一个有符号的、固定长度的时间跨度(例如,42 秒)。
  • timestamp 是一个独立于任何时区或日历的时间点(例如,2017-01-15T01:30:15.01Z)。
  • interval 是一个独立于时区或日历的时间间隔(例如,2017-01-15T01:30:15.01Z - 2017-01-16T02:30:15.01Z)。
  • date 是一个完整的日历日期(例如,2005-09-19)。
  • month 是一年中的月份(例如,四月)。
  • dayofweek 是一周中的一天(例如,星期一)。
  • timeofday 是一天中的时间(例如,10:42:23)。
  • field_mask 是一组符号字段路径(例如,f.b.d)。
  • postal_address 是一个邮政地址(例如,1600 Amphitheatre Parkway Mountain View, CA 94043 USA)。
  • money 是一定数量的货币及其货币类型(例如,42 美元)。
  • latlng 是一个纬度/经度对(例如,37.386051 纬度和 -122.083855 经度)。
  • color 是 RGBA 颜色空间中的颜色。

应该 在单独的文件中定义消息类型

在定义 Proto 架构时,每个文件应包含单个消息、枚举、扩展、服务或循环依赖项组。这使得重构更容易。当文件被分隔时,移动文件比从包含其他消息的文件中提取消息容易得多。遵循此实践还有助于保持 Proto 架构文件更小,从而提高可维护性。

如果它们将在您的项目之外被广泛使用,请考虑将它们放在自己的文件中,而不包含任何依赖项。这样,任何人都可以轻松使用这些类型,而不会引入其他 Proto 文件中的传递依赖项。

有关此主题的更多信息,请参阅 1-1-1 规则

不要 更改字段的默认值

几乎永远不要更改 Proto 字段的默认值。这会导致客户端和服务器之间的版本偏差。当客户端读取未设置的值时,与服务器读取相同的未设置值时,他们的构建跨越 Proto 更改时,将看到不同的结果。Proto3 删除了设置默认值的功能。

不要 从 Repeated 变为 Scalar

虽然它不会导致崩溃,但您会丢失数据。对于 JSON,重复性的不匹配将丢失整个消息。对于数字 proto3 字段和 proto2 packed 字段,从 repeated 变为 scalar 将丢失该字段中的所有数据。对于非数字 proto3 字段和未注释的 proto2 字段,从 repeated 变为 scalar 将导致最后一个反序列化的值“获胜”。

从 scalar 变为 repeated 在 proto2 和带有 [packed=false] 的 proto3 中是 OK 的,因为对于二进制序列化,标量值会变成一个单元素列表。

应该 遵循生成代码的风格指南

Proto 生成的代码在普通代码中被引用。确保 .proto 文件中的选项不会导致生成违反风格指南的代码。例如

不要 将文本格式消息用于互换

基于文本的序列化格式(如文本格式和 JSON)将字段和枚举值表示为字符串。因此,当字段或枚举值被重命名,或者添加新的字段或枚举值或扩展时,使用旧代码反序列化这些格式的协议缓冲区将失败。尽可能使用二进制序列化进行数据交换,仅将文本格式用于人工编辑和调试。

如果您在 API 中或用于存储数据时使用转换为 JSON 的 Proto,则可能根本无法安全地重命名字段或枚举。

永远不要 依赖跨构建的序列化稳定性

Proto 序列化的稳定性不能跨二进制文件或相同二进制文件的构建来保证。在构建缓存键时,例如,不要依赖它。

不要 在与其他代码相同的 Java 包中生成 Java Proto

将生成的 Java Proto 源代码放入与手写 Java 源代码不同的包中。packagejava_packagejava_alt_api_package 选项控制 生成的 Java 源代码的发出位置。确保手写 Java 源代码也不要位于同一包中。一种常见的做法是将您的 Proto 生成到项目中的 proto 子包中,该子包包含这些 Proto(即,没有手写源代码)。

避免对字段名称使用语言关键字

如果消息、字段、枚举或枚举值的名称是读取/写入该字段的语言中的关键字,则 Protobuf 可能会更改字段名称,并且可能具有与普通字段不同的访问方式。例如,请参阅 有关 Python 的此警告

您还应避免在文件路径中使用关键字,因为这也可能导致问题。

附录

API 最佳实践

本文档仅列出了极有可能导致中断的更改。有关如何编写优雅增长的 Proto API 的更高级别指南,请参阅 API 最佳实践