凌云的博客

行胜于言

Proto 最佳实践[译]

分类:proto| 发布时间:2025-01-09 08:55:00

原文:Proto Best Practices

客户端和服务器永远不会同时更新 - 即使您尝试同时更新它们。 其中一个可能会回滚。 不要因为客户端和服务器是同步的更新的就以为您可以进行破坏性更改。

不要重复使用标签号

切勿重复使用标签号。这会搞乱反序列化。 即使您认为没有人使用该字段,也不要重复使用标签号。 你需要记录使用过的 proto 标签号。 不然其他服务器的旧代码由于使用了重复的标签号可能会导致应用无法工作。

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

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

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

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

不要更改字段的类型

几乎永远不要更改字段的类型;它会搞乱反序列化,就像重复使用标签号一样。 protobuf 文档概述了少数可以接受的情况(例如,在 int32、uint32、int64 和 bool 之间切换)。 但是,除非新消息是旧消息的超集,否则更改字段的消息类型将导致不兼容。

不要添加必填字段

永远不要添加必填字段,而是添加 // required 来记录 API 的约定。 必填字段被很多人认为是有害的,因此已从 proto3 中完全删除。 将所有字段设为可选或重复。 您永远不知道消息类型会持续多长时间,也不知道四年后当它不再是逻辑上必需的但 proto 仍然说它是必需的时,是否有人会被迫用空字符串或零来填充您的必填字段。

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

不要在 message 中使用大量字段

不要在 message 中使用“大量”(可以想象为数百个)字段。 在 C++ 中,每个字段都会为内存对象大小增加大约 65 比特,无论它是否已填充(指针为 8 个字节,如果该字段被声明为可选,则位字段中的另一个位会跟踪该字段是否已设置)。 当您的原型变得太大时,生成的代码甚至可能无法编译(例如,在 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_epoch 或 int64 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 是一年中的月份(例如 4 月)。
  • dayofweek 是星期几(例如星期一)。
  • timeofday 是一天中的时间(例如 10:42:23)。
  • field_mask 是一组符号字段路径(例如 f.b.d)。
  • postal_address 是邮政地址(例如 1600 Amphitheatre Parkway Mountain View, CA 94043 USA)。
  • money 是金额及其货币类型(例如 42 USD)。
  • latlng 是纬度/经度对(例如 37.386051 纬度和 -122.083855 经度)。
  • color 是 RGBA 颜色空间中的颜色

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

定义 proto 时,每个文件应该只有一个消息、枚举、扩展、服务或一组循环依赖项。 这样可以更轻松地进行重构。 将文件分开移动比从包含其他消息的文件中提取消息要容易得多。 遵循此做法还有助于使原型架构文件更小,从而增强可维护性。

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

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

不要更改字段的默认值

大部分情况下不要更改 proto 字段的默认值。 这会导致客户端和服务器之间产生版本偏差。 当客户端和服务端使用不同的 proto 版本时,对于未设置的值,客户端和服务端可能会看到不同的结果。 Proto3 删除了设置默认值的功能

不要将 repeated 字段改成标量

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

在 proto2 和 proto3 中,使用 [packed=false] 将标量变为重复是可以的,因为对于二进制序列化,标量值变为一个元素列表。

遵循生成代码的样式指南

Proto 生成的代码在正常代码中引用。 确保 .proto 文件中的选项不会导致生成违反样式指南的代码。例如:

java_outer_classname 应遵循 https://google.github.io/styleguide/javaguide.html#s5.2.2-class-names

java_package 和 java_alt_package 应遵循 https://google.github.io/styleguide/javaguide.html#s5.2.1-package-names

package 虽然在 java_package 不存在时用于 Java,但始终直接对应于 C++ 命名空间,因此应遵循 https://google.github.io/styleguide/cppguide.html#Namespace_Names。 如果这些样式指南发生冲突,请使用 java_package 进行 Java 开发。

ruby_package 应采用 Foo::Bar::Baz 格式,而不是 Foo.Bar.Baz

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

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

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

切勿在不同构建中依赖序列化稳定性

proto 无法保证在不同的可执行文件或者不同构建中对相同的 protos 序列化为相同的二进制串。 因此不要依赖它,例如,在构建缓存键时。

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

将 Java proto 源代码生成到与手写 Java 源代码不同的包中。 package、java_package 和 java_alt_api_package 选项控制生成的 Java 源代码的输出位置。 确保手写的 Java 源代码不与生成的 Java 源代码处于同一包中。 一种常见做法是将 proto 文件生成到项目中的一个专用 proto 子包,该子包仅包含这些 proto 文件(即不包含手写的源代码)

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

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

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

附录

API 最佳实践

本文档仅列出极有可能导致不兼容的更改。 有关如何制作优雅扩展的原型 API 的更高级别指导,请参阅 API 最佳实践