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 中,方法大小有硬性限制)。

在枚举中包含一个未指定的值

枚举应在声明中将默认的 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 删除了设置默认值的功能。

不要从重复类型改为标量类型

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

从标量类型更改为重复类型在 proto2 中以及在 proto3 中使用[packed=false]时是可以的,因为对于二进制序列化,标量值将成为一个包含一个元素的列表。

遵循生成代码的风格指南

在普通代码中会引用 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 最佳实践