Proto 最佳实践

分享编写 Protocol Buffers 的经验证的最佳实践。

客户端和服务器永远不会在同一时间完全更新——即使您尝试同时更新它们。其中一个可能会回滚。不要假设您可以进行一个破坏性更改,然后因为客户端和服务器同步了就没问题。

不要 重复使用标签号

切勿重复使用标签号。这会破坏反序列化。即使您认为没有人使用该字段,也不要重复使用标签号。如果更改曾经上线过,您的 proto 的序列化版本可能仍在某个日志中。或者另一台服务器中可能存在旧代码会因此中断。

为已删除字段保留标签号

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

为已删除枚举值保留数字

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

将新的枚举别名放在最后

添加新的枚举别名时,将新名称放在最后,以便服务有时间接收它。

要安全地移除原始名称(如果它用于数据交换,但它不应该),您必须执行以下步骤:

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

  • 在所有解析器部署了该 schema 后,交换这两个名称的顺序(序列化器将开始使用新名称,解析器接受两者)

  • 在所有序列化器都使用了该版本的 schema 后,您可以删除弃用的名称。

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

不要 更改字段类型

几乎永远不要更改字段类型;这会破坏反序列化,就像重复使用标签号一样。 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 的 #include 语句之前,可能会导致编译错误。避免使用诸如“NULL”、“NAN”和“DOMAIN”之类的宏常量作为枚举值。

使用知名类型和常用类型

强烈建议使用以下常见共享类型。例如,当存在一个完全适合的常见类型时,不要在代码中使用 int32 timestamp_seconds_since_epochint64 timeout_millis

  • duration 是带符号的固定长度时间跨度(例如,42s)。
  • 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 schema 时,每个文件应包含一个消息、枚举、扩展、服务或一组循环依赖项。这使得重构更加容易。将分离的文件移动比从包含其他消息的文件中提取消息容易得多。遵循此实践也有助于保持 proto schema 文件较小,从而提高可维护性。

如果它们将在您的项目外部广泛使用,请考虑将它们放在没有依赖项的单独文件中。这样,任何人都可以轻松使用这些类型,而无需引入您其他 proto 文件中的传递性依赖项。

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

不要 更改字段的默认值

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

不要 从 Repeated 更改为 Scalar

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

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

遵循生成代码的风格指南

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

不要 使用文本格式消息进行数据交换

文本格式和 JSON 等基于文本的序列化格式将字段和枚举值表示为字符串。因此,当字段或枚举值被重命名,或添加新的字段、枚举值或扩展时,使用旧代码对这些格式的 protocol buffers 进行反序列化将会失败。可能时请使用二进制序列化进行数据交换,仅将文本格式用于人工编辑和调试。

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

绝不 依赖跨构建版本的序列化稳定性

不保证 proto 序列化在不同的二进制文件之间或同一二进制文件的不同构建版本之间保持稳定。例如,在构建缓存键时,请勿依赖它。

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

将 Java proto 源文件生成到与您的手写 Java 源文件不同的包中。packagejava_packagejava_alt_api_package 选项控制生成 Java 源文件的位置。确保手写 Java 源代码不与它们位于同一包中。一种常见做法是将您的 proto 生成到您项目中的 proto 子包中,该子包包含这些 proto(即,不包含手写源代码)。

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

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

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

使用 java_outer_classname

每个 proto schema 定义文件都应将选项 java_outer_classname 设置为转换为 TitleCase 并移除 ‘.’ 的 .proto 文件名。例如,文件 student_record_request.proto 应设置

option java_outer_classname = "StudentRecordRequestProto";

附录

API 最佳实践

本文档仅列出了极有可能导致破坏的更改。有关如何构建可优雅演进的 proto API 的更高级别指导,请参阅 API 最佳实践