样式指南

为如何最好地组织您的 proto 定义提供指导。

本文档提供了 .proto 文件的风格指南。遵循这些约定,您将使您的 protocol buffer 消息定义及其对应的类保持一致且易于阅读。

标准文件格式

  • 保持行长为 80 个字符。
  • 使用 2 个空格的缩进。
  • 字符串优先使用双引号。

文件结构

文件应命名为 lower_snake_case.proto

所有文件都应按以下顺序组织:

  1. 许可证头部(如果适用)
  2. 文件概述
  3. 语法
  4. 导入(已排序)
  5. 文件选项
  6. 其他所有内容

标识符命名风格

Protobuf 标识符使用以下命名风格之一:

  1. TitleCase
    • 包含大写字母、小写字母和数字
    • 首字符为大写字母
    • 每个单词的首字母大写
  2. lower_snake_case
    • 包含小写字母、下划线和数字
    • 单词之间用单个下划线分隔
  3. UPPER_SNAKE_CASE
    • 包含大写字母、下划线和数字
    • 单词之间用单个下划线分隔
  4. camelCase
    • 包含大写字母、小写字母和数字
    • 首字符为小写字母
    • 后续每个单词的首字母大写
    • 注意:下面的风格指南中,.proto 文件里的任何标识符都不使用 camelCase;这里澄清术语只是因为某些语言生成的代码可能会将标识符转换为这种风格。

在所有情况下,都将缩写词视为单个单词:使用 GetDnsRequest 而不是 GetDNSRequest,使用 dns_request 而不是 d_n_s_request

标识符中的下划线

不要在名称的开头或结尾使用下划线。任何下划线后面都应紧跟一个字母(而不是数字或第二个下划线)。

此规则的动机在于,每个 protobuf 语言实现都可能将标识符转换为本地语言的风格:.proto 文件中的名称 song_id 最终可能会有 SongIdsongIdsong_id 等不同风格的字段访问器,具体取决于语言。

通过仅在字母前使用下划线,可以避免名称在一种风格中是唯一的,但在转换为其他风格后发生冲突的情况。

例如,DNS2DNS_2 都会转换为 TitleCase 风格的 Dns2。允许使用这两种名称中的任何一种都可能导致痛苦的局面:当一个消息最初只在某些语言中使用,其生成的代码保留了原始的 UPPER_SNAKE_CASE 风格,并得到广泛应用,而后来才在另一种语言中使用,而该语言会将名称转换为 TitleCase 风格,从而导致冲突。

应用此风格规则意味着您应该使用 XYZ2XYZ_V2,而不是 XYZ_2XYZ_2V

包(Packages)

包名(package names)使用点分隔的 lower_snake_case 命名。

多词包名可以是 lower_snake_case 或 dot.delimited(点分隔的包名在大多数语言中会生成为嵌套的包/命名空间)。

包名应尽量基于项目名称,力求简短但唯一。包名不应是 Java 包(com.x.y);而应使用 x.y 作为包名,并根据需要使用 java_package 选项。

消息名称

消息名称使用 TitleCase 风格。

message SongRequest {
}

字段名称

字段名称,包括扩展(extensions),使用 snake_case 风格。

repeated 字段使用复数形式命名。

string song_name = 1;
repeated Song songs = 2;

Oneof 名称

oneof 名称使用 lower_snake_case 风格。

oneof song_id {
  string song_human_readable_id = 1;
  int64 song_machine_id = 2;
}

枚举(Enums)

枚举类型名称使用 TitleCase 风格。

枚举值名称使用 UPPER_SNAKE_CASE 风格。

enum FooBar {
  FOO_BAR_UNSPECIFIED = 0;
  FOO_BAR_FIRST_VALUE = 1;
  FOO_BAR_SECOND_VALUE = 2;
}

列出的第一个值应该是零值枚举,并带有 _UNSPECIFIED_UNKNOWN 的后缀。此值可用作未知/默认值,并且应与您期望显式设置的任何语义值区分开来。有关未指定枚举值的更多信息,请参阅 Proto 最佳实践页面

枚举值前缀

枚举值在语义上被认为不受其所属枚举名称的范围限制,因此不允许在两个兄弟枚举中使用相同的名称。例如,以下代码将被 protoc 拒绝,因为在两个枚举中定义的 SET 值被认为在同一作用域内:

enum CollectionType {
  COLLECTION_TYPE_UNSPECIFIED = 0;
  SET = 1;
  MAP = 2;
  ARRAY = 3;
}

// Won't compile - `SET` enum name will clash
// with the one defined in `CollectionType` enum.
enum TennisVictoryType {
  TENNIS_VICTORY_TYPE_UNSPECIFIED = 0;
  GAME = 1;
  SET = 2;
  MATCH = 3;
}

当枚举定义在文件的顶层(而不是嵌套在消息定义中)时,名称冲突的风险很高;在这种情况下,兄弟枚举包括在设置了相同包(package)的其他文件中定义的枚举,而 protoc 可能无法在代码生成时检测到冲突的发生。

为避免这些风险,强烈建议执行以下操作之一:

  • 为每个值加上枚举名称(转换为 UPPER_SNAKE_CASE)作为前缀
  • 将枚举嵌套在包含它的消息中

任一选项都足以减轻冲突风险,但首选带有前缀值的顶层枚举,而不是仅仅为了缓解问题而创建一个消息。由于某些语言不支持在“结构体”类型中定义枚举,因此首选带前缀的值可确保在各种绑定语言中采用一致的方法。

服务(Services)

服务名称和方法名称使用 TitleCase 风格。

service FooService {
  rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
  rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}

应避免的事项

Required 字段

Required 字段是一种强制机制,要求在解析线路字节时必须设置给定的字段,否则拒绝解析该消息。这种 required 的不变性通常不会在内存中构造的消息上强制执行。Required 字段在 proto3 中已被移除。已迁移到 editions 2023 的 Proto2 required 字段可以使用将 field_presence 功能设置为 LEGACY_REQUIRED 来适应。

虽然在模式(schema)级别强制执行 required 字段直观上是可取的,但 protobuf 的主要设计目标之一是支持长期的模式演进。无论某个字段今天看起来多么明显是必需的,在未来都有可能不再需要设置该字段(例如,一个 int64 user_id 将来可能需要迁移到一个 UserId user_id)。

特别是在中间件服务器可能转发它们并不真正需要处理的消息的情况下,required 的语义已证明对那些长期演进目标过于有害,因此现在非常不鼓励使用它。

请参阅强烈不推荐使用 Required

Groups

Groups 是嵌套消息的一种替代语法和线路格式。Groups 在 proto2 中被视为已弃用,在 proto3 中已被移除,并在 edition 2023 中转换为分隔表示形式。您可以使用嵌套消息定义和该类型的字段来代替 group 语法,并使用 message_encoding 功能以实现线路兼容性。

请参阅groups