樣式指南

提供有關如何最好地構建 proto 定義的指導。

本文件為 .proto 檔案提供了一份樣式指南。遵循這些約定,您將使您的協議緩衝區訊息定義及其相應的類保持一致且易於閱讀。

以下樣式指南的強制執行透過 enforce_naming_style 進行控制。

標準檔案格式

  • 行長保持在 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
    • 包含大寫字母、小寫字母和數字
    • 首字母為小寫字母
    • 後續每個單詞的首字母大寫
    • 注意:下面的樣式指南不將 camelCase 用於 .proto 檔案中的任何識別符號;此處僅澄清該術語,因為某些語言生成的程式碼可能會將識別符號轉換為此樣式。

在所有情況下,將縮寫視為單個單詞:使用 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)

包名應是由點分隔的 lower_snake_case 名稱序列。它們不應包含大寫字母。

多單詞包名可以是 lower_snake_case 或 dot.delimited(點分隔的包名在大多數語言中會作為巢狀包/名稱空間發出)。

包名應嘗試基於專案名稱使用一個簡短但唯一的名稱。包不應與目錄路徑耦合,尤其是在檔案位於深度巢狀路徑中時。

包名不應是 Java 包(com.company.x.y);而是使用 x.y 作為包,並使用 java_package 選項。

訊息名稱

訊息名稱使用 TitleCase。

message SongRequest {
}

欄位名稱

欄位名稱(包括副檔名)使用 snake_case。

重複欄位使用複數名稱。

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;
}

列舉

列舉型別名稱使用 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;
}

當列舉在檔案頂層(未巢狀在訊息定義中)定義時,名稱衝突的風險很高;在這種情況下,同級包括在設定相同包的其他檔案中定義的列舉,protoc 可能無法在程式碼生成時檢測到衝突。

為了避免這些風險,強烈建議執行以下操作之一

  • 每個值都以列舉名稱(轉換為 UPPER_SNAKE_CASE)作為字首
  • 將列舉巢狀在包含訊息中

任一選項都足以緩解衝突風險,但優先使用帶有字首值的頂級列舉,而不是僅僅為了緩解問題而建立訊息。由於某些語言不支援在“結構”型別中定義列舉,因此優先使用帶字首的值可確保跨繫結語言採用一致的方法。

在列舉值加字首時,剝離字首後的其餘名稱仍應是合法且符合樣式的列舉名稱。例如,避免以下情況:

enum DeviceTier {
  DEVICE_TIER_UNKNOWN = 0;
  DEVICE_TIER_1 = 1;
  DEVICE_TIER_2 = 2;
}

相反,使用像 DEVICE_TIER_TIER1 這樣的值名稱,其中 DEVICE_TIER_ 部分被視為對列舉值進行作用域限定,而不是作為單個列舉值名稱的一部分。一些 Protobuf 實現會自動剝離與包含列舉名稱匹配的字首(在安全的情況下),但在本例中不能,因為裸露的 1 不是合法的列舉值名稱。

未來的版本將增加對帶作用域列舉的支援,這將消除手動為每個列舉值新增字首的需要,並使其能夠簡潔地寫為 TIER1 = 1

服務(Services)

服務名稱和方法名稱使用 TitleCase。

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

應避免的事項

必填欄位

必填欄位是一種強制在解析二進位制資料時必須設定給定欄位的方法,否則拒絕解析訊息。必填不變數通常不強制執行記憶體中構建的訊息。必填欄位在 proto3 中已刪除。已遷移到 2023 版本的 Proto2 required 欄位可以使用 field_presence 功能集為 LEGACY_REQUIRED 以適應。

雖然在模式級別強制執行必填欄位在直觀上是可取的,但 protobuf 的主要設計目標之一是支援長期模式演進。無論今天給定欄位看起來多麼明顯是必需的,未來都可能出現該欄位不再需要設定的情況(例如,int64 user_id 可能需要在未來遷移到 UserId user_id)。

特別是在中介軟體伺服器可能轉發它們實際上不需要處理的訊息的情況下,required 的語義已被證明對這些長期演進目標造成了太大的危害,因此現在非常強烈地不鼓勵使用。

請參閱 強烈不建議使用 Required

Groups

組是巢狀訊息的替代語法和二進位制格式。組在 proto2 中被視為已棄用,在 proto3 中已刪除,並在 2023 版本中轉換為分隔表示。您可以使用巢狀訊息定義和該型別的欄位來代替使用組語法,使用 message_encoding 功能以實現二進位制相容性。

請參閱