Proto 最佳实践
客户端和服务器绝不会在完全相同的时间更新——即使您试图同时更新它们。其中之一可能会被回滚。不要假设您可以进行破坏性更改,并且因为客户端和服务器同步就没问题。
不要重用标签号
绝不要重用标签号。这会搞乱反序列化。即使您认为没有人使用该字段,也不要重用标签号。如果该更改曾经上线过,那么您的 proto 的序列化版本可能存在于某个日志中。或者,另一个服务器中可能存在会中断的旧代码。
要为已删除的字段保留标签号
当您删除不再使用的字段时,请保留其标签号,以便将来没有人会意外地重用它。只需 `reserved 2, 3;` 就足够了。不需要类型(这让您可以精简依赖!)。您还可以保留名称以避免回收现已删除的字段名:`reserved "foo", "bar";`。
要为已删除的枚举值保留编号
当您删除不再使用的枚举值时,请保留其编号,以便将来没有人会意外地重用它。只需 `reserved 2, 3;` 就足够了。您还可以保留名称以避免回收现已删除的值名称:`reserved "FOO", "BAR";`。
要将新的枚举别名放在最后
当您添加新的枚举别名时,请将新名称放在最后,以便服务有时间采纳它。
要安全地移除原始名称(如果它被用于交换,而它不应该被这样使用),您必须执行以下操作:
在旧名称下方添加新名称,并弃用旧名称(序列化器将继续使用旧名称)
在每个解析器都部署了该 schema 后,交换两个名称的顺序(序列化器将开始使用新名称,解析器接受两者)
在每个序列化器都拥有该版本的 schema 后,您可以删除已弃用的名称。
注意:虽然理论上客户端不应该使用旧名称进行交换,但遵循上述步骤仍然是礼貌的做法,特别是对于广泛使用的枚举名称。
不要更改字段的类型
几乎绝不要更改字段的类型;这会搞乱反序列化,与重用标签号一样。 protobuf 文档概述了少数可以接受的情况(例如,在 `int32`、`uint32`、`int64` 和 `bool` 之间转换)。然而,更改字段的消息类型将会中断,除非新消息是旧消息的超集。
不要添加 `required` 字段
绝不要添加 `required` 字段,而是添加 `// required` 来记录 API 契约。`required` 字段被许多人认为是有害的,以至于它们已从 proto3 中完全移除。将所有字段设为 `optional` 或 `repeated`。您永远不知道一个消息类型会持续多久,以及四年后当它在逻辑上不再是必需的时,是否有人会被迫用空字符串或零来填充您的 `required` 字段,而 proto 仍然规定它是必需的。
对于 proto3,没有 `required` 字段,因此此建议不适用。
不要创建包含大量字段的消息
不要创建包含“大量”(想象一下:数百个)字段的消息。在 C++ 中,每个字段都会给内存中的对象大小增加大约 65 位,无论它是否被填充(8 字节用于指针,如果字段声明为 optional,则在位域中还有一个位用于跟踪字段是否已设置)。当您的 proto 变得太大时,生成的代码甚至可能无法编译(例如,在 Java 中,方法的大小有硬性限制)。
要在枚举中包含一个未指定值
枚举应在其声明的第一个值中包含一个默认的 `FOO_UNSPECIFIED` 值。当新值被添加到枚举中时,旧客户端会将该字段视为未设置,并且 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` 是一个有符号的、固定长度的时间跨度(例如,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 USD)。
- `latlng` 是一个纬度/经度对(例如,纬度 37.386051 和经度 -122.083855)。
- `color` 是 RGBA 颜色空间中的一种颜色。
要在单独的文件中定义消息类型
在定义 proto schema 时,每个文件应只包含一个消息、枚举、扩展、服务或一组循环依赖。这使得重构更容易。当文件分离时,移动它们比从包含其他消息的文件中提取消息要容易得多。遵循这种做法也有助于保持 proto schema 文件较小,从而增强可维护性。
如果它们将在您的项目之外被广泛使用,请考虑将它们放在自己的、没有依赖关系的文件中。这样任何人都可以轻松使用这些类型,而无需引入您其他 proto 文件中的传递性依赖。
有关此主题的更多信息,请参阅 1-1-1 规则。
不要更改字段的默认值
几乎绝不要更改 proto 字段的默认值。这会导致客户端和服务器之间的版本偏差。当客户端和服务器的构建版本跨越 proto 更改时,读取未设置值的客户端将看到与读取相同未设置值的服务器不同的结果。Proto3 移除了设置默认值的功能。
不要从 `repeated` 改为标量
虽然这不会导致崩溃,但您会丢失数据。对于 JSON,`repeated` 属性的不匹配将导致整个*消息*丢失。对于数字类型的 proto3 字段和 proto2 的 `packed` 字段,从 `repeated` 改为标量将导致该*字段*中的所有数据丢失。对于非数字类型的 proto3 字段和未注解的 proto2 字段,从 `repeated` 改为标量将导致最后反序列化的值“获胜”。
在 proto2 和带有 `[packed=false]` 的 proto3 中,从标量改为 `repeated` 是可以的,因为对于二进制序列化,标量值会变成一个单元素的列表。
要遵循生成代码的风格指南
Proto 生成的代码在普通代码中被引用。请确保 `.proto` 文件中的选项不会导致生成违反风格指南的代码。例如:
`java_outer_classname` 应遵循 https://ggdocs.cn/styleguide/javaguide.html#s5.2.2-class-names
`java_package` 和 `java_alt_package` 应遵循 https://ggdocs.cn/styleguide/javaguide.html#s5.2.1-package-names
`package` 虽然在 `java_package` 不存在时用于 Java,但它总是直接对应于 C++ 命名空间,因此应遵循 https://ggdocs.cn/styleguide/cppguide.html#Namespace_Names。如果这些风格指南冲突,请为 Java 使用 `java_package`。
`ruby_package` 的形式应为 `Foo::Bar::Baz` 而不是 `Foo.Bar.Baz`。
不要使用文本格式消息进行交换
基于文本的序列化格式(如文本格式和 JSON)将字段和枚举值表示为字符串。因此,当字段或枚举值被重命名,或者添加了新的字段、枚举值或扩展时,使用旧代码对这些格式的 protocol buffer 进行反序列化将会失败。在可能的情况下,使用二进制序列化进行数据交换,而仅将文本格式用于人工编辑和调试。
如果您在 API 中或用于存储数据时使用转换为 JSON 的 proto,您可能根本无法安全地重命名字段或枚举。
绝不依赖跨构建版本的序列化稳定性
Proto 序列化的稳定性在不同二进制文件之间或同一二进制文件的不同构建版本之间不被保证。例如,在构建缓存键时不要依赖它。
不要在与其他代码相同的 Java 包中生成 Java Proto
将 Java proto 源文件生成到与您手写的 Java 源文件不同的包中。`package`、`java_package` 和 `java_alt_api_package` 选项控制生成的 Java 源文件的输出位置。请确保手写的 Java 源代码也不存在于该同一个包中。一种常见的做法是将您的 protos 生成到项目中的一个 `proto` 子包中,该子包**仅**包含那些 protos(即,没有手写的源代码)。
避免使用语言关键字作为字段名
如果消息、字段、枚举或枚举值的名称是读取/写入该字段的语言中的关键字,那么 protobuf 可能会更改字段名称,并且访问它们的方式可能与普通字段不同。例如,请参阅关于 Python 的这个警告。
您还应避免在文件路径中使用关键字,因为这也会导致问题。
要为 RPC API 和存储使用不同的消息
为 API 和长期存储重用相同的消息可能看起来很方便,可以减少样板代码和消息之间转换的开销。
然而,长期存储和实时 RPC 服务的需求往往会在后期出现分歧。即使它们最初在很大程度上是重复的,使用独立的类型也能让您自由地更改存储格式,而不会影响您的外部客户端。对您的代码进行分层,以便模块要么处理客户端 protos,要么处理存储 protos,要么处理转换。
维护转换层是有成本的,但一旦您有了客户并且必须进行第一次存储更改,这种成本很快就会得到回报。
不要对当前只有两种状态但将来可能更多的东西使用布尔值
如果您为一个字段使用布尔值,请确保该字段确实只描述两种可能的状态(在所有时间内,而不仅仅是现在和不远的将来)。使用枚举的未来灵活性通常是值得的,即使它在首次引入时只有两个值。
message Photo {
// Bad: True if it's a GIF.
optional bool gif;
// Good: File format of the referenced photo (for example, GIF, WebP, PNG).
optional PhotoType type;
}
要使用 `java_outer_classname`
每个 proto schema 定义文件都应将选项 `java_outer_classname` 设置为将 `.proto` 文件名转换为首字母大写的驼峰式(TitleCase)并移除“.”。例如,文件 `student_record_request.proto` 应设置
option java_outer_classname = "StudentRecordRequestProto";