語言指南 (editions)

介紹如何在專案中使用 Protocol Buffers 語言的版本修訂。

本指南介紹瞭如何使用協議緩衝區語言來構建協議緩衝區資料,包括 .proto 檔案語法以及如何從 .proto 檔案生成資料訪問類。它涵蓋了協議緩衝區語言的 2023 版2024 版。有關版本與 proto2 和 proto3 在概念上如何不同,請參閱 Protobuf 版本概述

有關 proto2 語法的資訊,請參閱 Proto2 語言指南

有關 proto3 語法的資訊,請參閱 Proto3 語言指南

這是一份參考指南——若想透過一個分步示例來了解本文件中描述的許多功能,請參閱你所選語言的教程

定義訊息型別

首先,我們來看一個非常簡單的例子。假設您想定義一個搜尋請求訊息格式,其中每個搜尋請求都有一個查詢字串、您感興趣的特定結果頁面以及每頁的結果數量。這是您用來定義訊息型別的 .proto 檔案。

edition = "2023";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}
  • 檔案的第一行指定您正在使用 protobuf 語言規範的 2023 版。

    • edition(或 proto2/proto3 的 syntax)必須是檔案的第一個非空、非註釋行。
    • 如果未指定 editionsyntax,協議緩衝區編譯器將假定您正在使用 proto2
  • SearchRequest 訊息定義指定了三個欄位(名稱/值對),每種你想包含在此類訊息中的資料都對應一個欄位。每個欄位都有一個名稱和一個型別。

指定欄位型別

在前面的例子中,所有欄位都是標量型別:兩個整數(page_numberresults_per_page)和一個字串(query)。您還可以為您的欄位指定列舉和複合型別,例如其他訊息型別。

分配欄位編號

你必須為訊息定義中的每個欄位賦予一個介於 1536,870,911 之間的編號,並遵守以下限制:

  • 給定的編號必須在該訊息的所有欄位中是唯一的
  • 欄位編號 19,00019,999 為 Protocol Buffers 實現保留。如果你在訊息中使用這些保留的欄位編號,protocol buffer 編譯器會報錯。
  • 您不能使用任何之前保留的欄位編號或已分配給擴充套件的任何欄位編號。

一旦您的訊息型別投入使用,這個編號就不能更改,因為它在訊息的有線格式中標識該欄位。“更改”欄位編號等同於刪除該欄位並建立一個具有相同型別但編號不同的新欄位。有關如何正確執行此操作,請參閱刪除欄位

欄位編號永遠不應被重用。切勿將一個欄位編號從保留列表中移出,用於新的欄位定義。請參閱重用欄位編號的後果

您應該為最常設定的欄位使用 1 到 15 的欄位編號。較小的欄位編號值在有線格式中佔用更少的空間。例如,範圍在 1 到 15 之間的欄位編號需要一個位元組進行編碼。範圍在 16 到 2047 之間的欄位編號需要兩個位元組。您可以在Protocol Buffer 編碼中找到更多相關資訊。

重用欄位編號的後果

重用欄位編號會使解碼線路格式的訊息變得模稜兩可。

protobuf 線路格式是精簡的,沒有提供檢測使用一種定義編碼的欄位並用另一種定義解碼的方法。

使用一種定義編碼一個欄位,然後用不同的定義解碼同一個欄位可能導致:

  • 開發者浪費時間除錯
  • 解析/合併錯誤(最好的情況)
  • 個人身份資訊/敏感個人身份資訊洩露
  • 資料損壞

欄位編號重用的常見原因

  • 重編號欄位(有時為了實現欄位編號順序更美觀)。重編號實際上是刪除並重新新增所有涉及的欄位,導致不相容的線路格式變更。

  • 刪除一個欄位並且沒有保留其編號以防止未來重用。

欄位編號限制為 29 位而不是 32 位,因為有三位用於指定欄位的線路格式。有關更多資訊,請參閱編碼主題

指定欄位基數

訊息欄位可以是以下之一:

  • 單一 (Singular):

    一個 singular 欄位沒有顯式基數標籤。它有兩種可能的狀態

    • 該欄位已設定,幷包含一個被顯式設定或從線路中解析出的值。它將被序列化到線路中。
    • 該欄位未設定,並將返回預設值。它將不會被序列化到線路中。

    你可以檢查該值是否被顯式設定。

    已遷移到版本的 Proto3 隱式欄位將使用設定為 IMPLICIT 值的 field_presence 功能集。

    已遷移到版本的 Proto2 required 欄位也將使用 field_presence 功能,但設定為 LEGACY_REQUIRED

  • repeated:此欄位型別在一個格式良好的訊息中可以重複零次或多次。重複值的順序將被保留。

  • map:這是一個鍵值對欄位型別。有關此欄位型別的更多資訊,請參閱對映

重複欄位預設打包

在 proto 版本中,標量數字型別的 repeated 欄位預設使用 packed 編碼。

你可以在 Protocol Buffer 編碼中找到更多關於 packed 編碼的資訊。

格式良好的訊息

術語“格式良好”在應用於 protobuf 訊息時,指的是序列化/反序列化的位元組。protoc 解析器會驗證給定的 proto 定義檔案是否可解析。

Singular 欄位可以在有線格式位元組中出現多次。解析器會接受輸入,但只有該欄位的最後一個例項可以透過生成的繫結訪問。有關此主題的更多資訊,請參閱後者為準

新增更多訊息型別

可以在單個 .proto 檔案中定義多個訊息型別。如果您正在定義多個相關的訊息,這很有用——例如,如果您想定義與您的 SearchResponse 訊息型別相對應的回覆訊息格式,您可以將其新增到同一個 .proto 檔案中:

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

組合訊息會導致臃腫 雖然可以在單個 .proto 檔案中定義多種訊息型別(如 message、enum 和 service),但當在單個檔案中定義大量具有不同依賴關係的訊息時,也可能導致依賴臃腫。建議在每個 .proto 檔案中包含儘可能少的訊息型別。

添加註釋

要為你的 .proto 檔案添加註釋:

  • 優先使用 C/C++/Java 風格的行尾註釋 '//',放在 .proto 程式碼元素的前一行。

  • 也接受 C 風格的內聯/多行註釋 /* ... */

    • 使用多行註釋時,推薦使用 '*' 作為邊距行。
/**
 * SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response.
 */
message SearchRequest {
  string query = 1;

  // Which page number do we want?
  int32 page_number = 2;

  // Number of results to return per page.
  int32 results_per_page = 3;
}

刪除欄位

如果操作不當,刪除欄位可能會導致嚴重問題。

當您不再需要某個欄位並且已從客戶端程式碼中刪除所有引用時,您可以從訊息中刪除該欄位定義。但是,您必須保留已刪除的欄位編號。如果您不保留欄位編號,開發人員將來可能會重用該編號。

你也應該保留欄位名稱,以允許你的訊息的 JSON 和 TextFormat 編碼繼續解析。

保留欄位編號

如果您透過完全刪除欄位或將其註釋掉來更新訊息型別,未來的開發人員在對該型別進行自己的更新時可以重用該欄位編號。這可能會導致嚴重問題,如重用欄位編號的後果中所述。為確保這種情況不會發生,請將您刪除的欄位編號新增到 reserved 列表中。

如果未來的開發者試圖使用這些保留的欄位編號,protoc 編譯器將生成錯誤訊息。

message Foo {
  reserved 2, 15, 9 to 11;
}

保留的欄位編號範圍是包含性的(9 to 119, 10, 11 相同)。

保留欄位名

之後重用舊欄位名通常是安全的,除非在使用 TextProto 或 JSON 編碼時,欄位名會被序列化。為避免這種風險,你可以將被刪除的欄位名新增到 reserved 列表中。

保留名稱僅影響 protoc 編譯器的行為,而不影響執行時行為,但有一個例外:TextProto 實現可能會在解析時丟棄具有保留名稱的未知欄位(而不會像其他未知欄位那樣引發錯誤)(目前只有 C++ 和 Go 實現這樣做)。執行時 JSON 解析不受保留名稱的影響。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved foo, bar;
}

請注意,你不能在同一個 reserved 語句中混合使用欄位名和欄位編號。

從你的 .proto 檔案生成了什麼?

當您對 .proto 檔案執行protocol buffer 編譯器時,編譯器會以您選擇的語言生成您需要的程式碼,用於處理您在檔案中描述的訊息型別,包括獲取和設定欄位值、將訊息序列化到輸出流以及從輸入流解析訊息。

  • 對於 C++,編譯器會從每個 .proto 檔案生成一個 .h 和一個 .cc 檔案,其中為你檔案中描述的每種訊息型別都提供一個類。
  • 對於 Java,編譯器會為每種訊息型別生成一個包含一個類的 .java 檔案,以及一個用於建立訊息類例項的特殊 Builder 類。
  • 對於 Kotlin,除了 Java 生成的程式碼外,編譯器還為每種訊息型別生成一個 .kt 檔案,其中包含一個改進的 Kotlin API。這包括一個簡化訊息例項建立的 DSL、一個可空欄位訪問器和一個複製函式。
  • Python 有點不同——Python 編譯器會生成一個模組,其中包含您 .proto 檔案中每種訊息型別的靜態描述符,然後該描述符與一個*元類 (metaclass)* 一起使用,以在執行時建立必要的 Python 資料訪問類。
  • 對於 Go,編譯器會生成一個 .pb.go 檔案,其中包含你檔案中每種訊息型別的一個型別。
  • 對於 Ruby,編譯器會生成一個 .rb 檔案,其中包含一個 Ruby 模組,該模組包含你的訊息型別。
  • 對於 Objective-C,編譯器會從每個 .proto 檔案生成一個 pbobjc.hpbobjc.m 檔案,其中為你檔案中描述的每種訊息型別都提供一個類。
  • 對於 C#,編譯器會從每個 .proto 檔案生成一個 .cs 檔案,其中為你檔案中描述的每種訊息型別都提供一個類。
  • 對於 PHP,編譯器為您檔案中描述的每種訊息型別生成一個 .php 訊息檔案,併為您編譯的每個 .proto 檔案生成一個 .php 元資料檔案。元資料檔案用於將有效的訊息型別載入到描述符池中。
  • 對於 Dart,編譯器會生成一個 .pb.dart 檔案,其中包含你檔案中每種訊息型別的一個類。

你可以透過按照你所選語言的教程來了解更多關於使用每種語言 API 的資訊。要了解更詳細的 API 資訊,請參閱相關的API 參考

標量值型別

一個標量訊息欄位可以有以下型別之一——該表顯示了在 .proto 檔案中指定的型別,以及在自動生成的類中對應的型別:

Proto 型別說明
double
float
int32使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint32。
int64使用可變長度編碼。對於編碼負數效率低下——如果你的欄位可能包含負值,請改用 sint64。
uint32使用可變長度編碼。
uint64使用可變長度編碼。
sint32使用可變長度編碼。有符號整數值。這些比常規的 int32 更高效地編碼負數。
sint64使用可變長度編碼。有符號整數值。這些比常規的 int64 更高效地編碼負數。
fixed32總是四個位元組。如果值經常大於 228,比 uint32 更高效。
fixed64總是八個位元組。如果值經常大於 256,比 uint64 更高效。
sfixed32總是四個位元組。
sfixed64總是八個位元組。
bool
string字串必須始終包含 UTF-8 編碼或 7 位 ASCII 文字,且長度不能超過 232
bytes可包含任意位元組序列,長度不超過 232
Proto 型別C++ 型別Java/Kotlin 型別[1]Python 型別[3]Go 型別Ruby 型別C# 型別PHP 型別Dart 型別Rust 型別
doubledoubledoublefloatfloat64Floatdoublefloatdoublef64
floatfloatfloatfloatfloat32Floatfloatfloatdoublef32
int32int32_tintintint32Fixnum 或 Bignum (根據需要)intintegerinti32
int64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
uint32uint32_tint[2]int/long[4]uint32Fixnum 或 Bignum (根據需要)uintintegerintu32
uint64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sint32int32_tintintint32Fixnum 或 Bignum (根據需要)intintegerinti32
sint64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
fixed32uint32_tint[2]int/long[4]uint32Fixnum 或 Bignum (根據需要)uintintegerintu32
fixed64uint64_tlong[2]int/long[4]uint64Bignumulonginteger/string[6]Int64u64
sfixed32int32_tintintint32Fixnum 或 Bignum (根據需要)intintegerinti32
sfixed64int64_tlongint/long[4]int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanboolbool
stringstringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytesstringByteStringstr (Python 2), bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] 為了確保在混合的 Java/Kotlin 程式碼庫中的相容性,Kotlin 即使對於無符號型別也使用 Java 對應的型別。

[2] 在 Java 中,無符號 32 位和 64 位整數使用其有符號的對應型別來表示,最高位僅儲存在符號位中。

[3] 在所有情況下,為欄位設定值都會進行型別檢查以確保其有效。

[4] 64 位或無符號 32 位整數在解碼時總是表示為 long,但在設定欄位時如果給定的是 int,也可以是 int。在所有情況下,設定的值必須符合所表示的型別。參見 [2]。

[5] Python 字串在解碼時表示為 unicode,但如果給定 ASCII 字串,也可以是 str(這可能會更改)。

[6] 在 64 位機器上使用 Integer,在 32 位機器上使用 string。

你可以在 Protocol Buffer 編碼中瞭解更多關於這些型別在序列化訊息時的編碼方式。

欄位預設值

當解析訊息時,如果編碼的訊息位元組中不包含某個特定欄位,那麼在解析的物件中訪問該欄位將返回該欄位的預設值。預設值是型別特定的:

  • 對於字串,預設值是空字串。
  • 對於位元組,預設值是空位元組。
  • 對於布林值,預設值是 false。
  • 對於數值型別,預設值是零。
  • 對於訊息欄位,該欄位未設定。其確切值取決於語言。詳情請參閱生成的程式碼指南
  • 對於列舉,預設值是第一個定義的列舉值,該值必須為 0。請參閱列舉預設值

對於 repeated 欄位,預設值是空的(通常是相應語言中的空列表)。

對於 map 欄位,預設值是空的(通常是相應語言中的空 map)。

覆蓋預設標量值

在 protobuf 版本中,您可以為奇異的非訊息欄位指定顯式預設值。例如,假設您希望為 SearchRequest.result_per_page 欄位提供預設值 10

int32 result_per_page = 3 [default = 10];

如果傳送方未指定 result_per_page,接收方將觀察到以下狀態

  • result_per_page 欄位不存在。也就是說,has_result_per_page()(存在器方法)方法將返回 false
  • result_per_page 的值(從“getter”返回)為 10

如果傳送方確實傳送了 result_per_page 的值,則忽略預設值 10,並從“getter”返回傳送方的值。

有關預設值在生成程式碼中如何工作的更多細節,請參閱你所選語言的生成程式碼指南

對於 field_presence 功能設定為 IMPLICIT 的欄位,不能指定顯式預設值。

列舉

當您定義訊息型別時,您可能希望它的某個欄位只能是預定義列表中的值之一。例如,假設您想為每個 SearchRequest 新增一個 corpus 欄位,其中 corpus 可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO。您可以透過在您的訊息定義中新增一個 enum 併為每個可能的值定義一個常量來非常簡單地實現這一點。

在下面的例子中,我們添加了一個名為 Corpusenum,包含了所有可能的值,以及一個型別為 Corpus 的欄位:

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

列舉預設值

SearchRequest.corpus 欄位的預設值是 CORPUS_UNSPECIFIED,因為這是列舉中定義的第一個值。

在 2023 版中,列舉定義中定義的第一個值必須為零,並且應該命名為 ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。這是因為

  • 零值需要作為第一個元素,以與 proto2 語義相容,在 proto2 中,除非顯式指定不同的值,否則第一個列舉值是預設值。
  • 必須有一個零值,以與 proto3 語義相容,在 proto3 中,零值用作使用此列舉型別的所有隱式存在欄位的預設值。

還建議這個第一個預設值除了“此值未指定”外,不具有任何語義含義。

SearchRequest.corpus 欄位這樣的列舉欄位的預設值可以像這樣顯式覆蓋

  Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

如果一個列舉型別已使用 option features.enum_type = CLOSED; 從 proto2 遷移,則列舉中第一個值沒有限制。不建議更改此類列舉的第一個值,因為它會更改使用該列舉型別但沒有顯式欄位預設值的任何欄位的預設值。

列舉值別名

您可以透過為不同的列舉常量賦予相同的值來定義別名。為此,您需要將 `allow_alias` 選項設定為 `true`。否則,當發現別名時,protocol buffer 編譯器會生成一條警告訊息。雖然所有別名值在序列化時都有效,但在反序列化時只使用第一個值。

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a warning message.
  ENAA_FINISHED = 2;
}

列舉常量

列舉器常量必須在 32 位整數的範圍內。由於 `enum` 值在傳輸時使用 varint 編碼,負值效率低下,因此不推薦使用。您可以在訊息定義中定義 `enum`,如前面的示例所示,也可以在外部定義——這些 `enum` 可以在您的 `.proto` 檔案中的任何訊息定義中重用。您還可以使用在一個訊息中宣告的 `enum` 型別作為另一個訊息中欄位的型別,使用語法 `_MessageType_._EnumType_`。

特定語言的列舉實現

當您對使用 `enum` 的 `.proto` 檔案執行 protocol buffer 編譯器時,生成的程式碼將為 Java、Kotlin 或 C++ 提供相應的 `enum`,或者為 Python 提供一個特殊的 `EnumDescriptor` 類,用於在執行時生成的類中建立一組帶有整數值的符號常量。

在反序列化過程中,無法識別的列舉值將保留在訊息中,儘管在反序列化訊息時如何表示取決於語言。在支援範圍外值的開放列舉型別(例如 C++ 和 Go)的語言中,未知列舉值簡單地儲存為其底層整數表示。在具有封閉列舉型別(例如 Java)的語言中,列舉中的一個 case 用於表示無法識別的值,並且底層整數可以透過特殊的訪問器訪問。在這兩種情況下,如果訊息被序列化,無法識別的值仍將與訊息一起序列化。

有關如何在你的應用程式中使用訊息 enum 的更多資訊,請參閱你所選語言的生成程式碼指南

保留值

如果您透過完全刪除一個列舉條目或將其註釋掉來更新一個列舉型別,未來的使用者在對該型別進行自己的更新時可以重用該數值。如果他們以後載入同一 .proto 的舊例項,這可能會導致嚴重問題,包括資料損壞、隱私漏洞等。確保這種情況不發生的一種方法是指定您刪除的條目的數值(和/或名稱,名稱也可能導致 JSON 序列化問題)是 reserved 的。如果任何未來的使用者試圖使用這些識別符號,protocol buffer 編譯器會報錯。您可以使用 max 關鍵字指定您的保留數值範圍一直到可能的最大值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved FOO, BAR;
}

請注意,你不能在同一個 reserved 語句中混合使用欄位名和數值。

使用其他訊息型別

您可以使用其他訊息型別作為欄位型別。例如,假設您希望在每個 SearchResponse 訊息中包含 Result 訊息——為此,您可以在同一個 .proto 檔案中定義一個 Result 訊息型別,然後在 SearchResponse 中指定一個型別為 Result 的欄位:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

匯入定義

在前面的例子中,Result 訊息型別與 SearchResponse 在同一個檔案中定義——如果你想用作欄位型別的訊息型別已經定義在另一個 .proto 檔案中,該怎麼辦?

你可以透過匯入來使用其他 .proto 檔案中的定義。要匯入另一個 .proto 的定義,你在檔案頂部新增一個 import 語句:

import "myproject/other_protos.proto";

從 2024 版開始,您還可以使用 import option 從其他 .proto 檔案使用自定義選項定義。與常規匯入不同,這隻允許使用自定義選項定義,而不允許使用其他訊息或列舉定義,以避免生成的程式碼中的依賴項。

import option "myproject/other_protos.proto";

預設情況下,您只能使用直接匯入的 .proto 檔案中的定義。然而,有時您可能需要將一個 .proto 檔案移動到新位置。您可以不在一次更改中直接移動 .proto 檔案並更新所有呼叫點,而是在舊位置放置一個佔位符 .proto 檔案,使用 import public 概念將所有匯入轉發到新位置。

請注意,公共匯入功能在 Java、Kotlin、TypeScript、JavaScript、GCL 以及使用 protobuf 靜態反射的 C++ 目標中不可用。

import public 依賴可以被任何匯入包含 import public 語句的 proto 的程式碼傳遞性地依賴。例如:

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

協議編譯器使用 -I/--proto_path 標誌在協議編譯器命令列指定的一組目錄中搜索匯入的檔案。如果沒有給定標誌,它會在呼叫編譯器的目錄中查詢。通常,您應該將 --proto_path 標誌設定為專案根目錄,並對所有匯入使用完全限定名稱。

符號可見性

當被其他 protos 匯入時,哪些符號可用或不可用,由 features.default_symbol_visibility 功能和 2024 版中新增的 exportlocal 關鍵字控制。

只有透過預設符號可見性或 export 關鍵字匯出的符號才能被匯入檔案引用。

使用 proto2 和 proto3 訊息型別

可以匯入 proto2proto3 訊息型別並在您的版本訊息中使用它們,反之亦然。

巢狀型別

你可以在其他訊息型別內部定義和使用訊息型別,如下例所示——這裡的 Result 訊息是在 SearchResponse 訊息內部定義的。

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果你想在其父訊息型別之外重用此訊息型別,你可以透過 _Parent_._Type_ 的方式引用它:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

你可以隨心所欲地巢狀訊息。在下面的例子中,請注意兩個名為 Inner 的巢狀型別是完全獨立的,因為它們是在不同的訊息中定義的:

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

更新訊息型別

如果現有的訊息型別不再滿足您的所有需求——例如,您希望訊息格式有一個額外的欄位——但您仍然想使用舊格式建立的程式碼,別擔心!當您使用二進位制有線格式時,更新訊息型別非常簡單,不會破壞任何現有程式碼。

請查閱 Proto 最佳實踐和以下規則:

二進位制線路非安全變更

有線不安全 (Wire-unsafe) 的更改是指,如果您使用新的 schema 解析器來解析使用舊 schema 序列化的資料(或反之),將會導致中斷的 schema 更改。只有在您知道資料的所有序列化器和反序列化器都使用新的 schema 時,才進行有線不安全的更改。

  • 更改任何現有欄位的欄位編號是不安全的。
    • 更改欄位編號等同於刪除該欄位並新增一個具有相同型別的新欄位。如果你想重編號一個欄位,請參閱刪除欄位的說明。
  • 將欄位移入一個現有的 oneof 是不安全的。

二進位制線路安全變更

線路安全的變更是指完全安全地演進模式,而不會有資料丟失或新的解析失敗的風險。

請注意,任何有線安全 (wire-safe) 的更改對於給定語言的應用程式碼來說都可能是破壞性更改。例如,向一個已有的列舉中新增一個值,對於任何對該列舉進行窮盡式 switch 的程式碼來說,都會導致編譯中斷。因此,Google 可能會避免在公共訊息上進行這類更改:AIPs 中包含了關於哪些更改是安全的指導意見。

  • 新增新欄位是安全的。
    • 如果您新增新欄位,任何由使用您的“舊”訊息格式的程式碼序列化的訊息仍然可以被您的新生成程式碼解析。您應該記住這些元素的預設值,以便新程式碼可以正確地與舊程式碼生成的訊息互動。同樣,由您的新程式碼建立的訊息可以被您的舊程式碼解析:舊的二進位制檔案在解析時會簡單地忽略新欄位。有關詳細資訊,請參閱未知欄位部分。
  • 移除欄位是安全的。
    • 在您更新的訊息型別中,不得再次使用相同的欄位編號。您可能需要重新命名欄位,例如新增字首“OBSOLETE_”,或者將欄位編號設為保留,這樣您 .proto 的未來使用者就不會意外地重用該編號。
  • 向列舉新增額外的值是安全的。
  • 將一個單一的顯式存在欄位或擴充套件變更為一個新的 oneof 的成員是安全的。
  • 將一個只包含一個欄位的 oneof 更改為顯式存在欄位是安全的。
  • 將一個欄位更改為具有相同編號和型別的擴充套件是安全的。

二進位制線路相容變更(有條件安全)

與有線安全 (Wire-safe) 的更改不同,有線相容 (wire-compatible) 意味著相同的資料在給定更改前後都可以被解析。然而,在這種型別的更改下,資料的解析可能會是有損的。例如,將一個 int32 更改為 int64 是一個相容的更改,但是如果寫入了一個大於 INT32_MAX 的值,一個將其作為 int32 讀取的客戶端將丟棄該數字的高位位元。

只有在您仔細管理系統推出過程的情況下,才能對您的 schema 進行相容性更改。例如,您可以將 int32 更改為 int64,但要確保在新的 schema 部署到所有端點之前,您繼續只寫入合法的 int32 值,然後在之後才開始寫入更大的值。

如果你的模式是在組織外部發布的,通常不應該進行線路相容的更改,因為你無法管理新模式的部署,從而無法知道何時使用不同範圍的值是安全的。

  • int32uint32int64uint64bool 都是相容的。
    • 如果從線路中解析出一個不適合相應型別的數字,你將得到與在 C++ 中將該數字強制轉換為該型別相同的效果(例如,如果一個 64 位數字被作為 int32 讀取,它將被截斷為 32 位)。
  • sint32sint64 彼此相容,但與其他整數型別相容。
    • 如果寫入的值在 INT_MIN 和 INT_MAX(含)之間,那麼用任一型別解析都會得到相同的值。如果寫入了一個超出該範圍的 sint64 值,並被解析為 sint32,則 varint 會被截斷為 32 位,然後進行 zigzag 解碼(這將導致觀察到不同的值)。
  • 只要位元組是有效的 UTF-8,stringbytes 就是相容的。
  • 如果位元組包含訊息的編碼例項,則嵌入式訊息與 bytes 相容。
  • fixed32sfixed32 相容,fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,singular 與 repeated 相容。
    • 對於重複欄位的序列化資料作為輸入,期望該欄位為 singular 的客戶端,如果它是原始型別欄位,將取最後一個輸入值;如果它是訊息型別欄位,將合併所有輸入元素。請注意,這對於數字型別(包括布林值和列舉)通常是安全的。數字型別的重複欄位預設以打包格式序列化,當期望的是 singular 欄位時,將無法正確解析。
  • enumint32uint32int64uint64 相容。
    • 請注意,當訊息被反序列化時,客戶端程式碼可能會以不同的方式處理它們:例如,未識別的 proto3 `enum` 值將保留在訊息中,但當訊息被反序列化時,其表示方式是依賴於語言的。
  • map<K, V> 和相應的 repeated 訊息欄位之間更改欄位是二進位制相容的(有關訊息佈局和其他限制,請參閱下面的對映)。
    • 然而,更改的安全性取決於應用程式:在反序列化和重新序列化訊息時,使用 `repeated` 欄位定義的客戶端將產生語義上相同的結果;但是,使用 `map` 欄位定義的客戶端可能會重新排序條目並丟棄具有重複鍵的條目。

未知欄位

未知欄位是格式良好的 protocol buffer 序列化資料,表示解析器無法識別的欄位。例如,當一箇舊的二進位制檔案解析一個由新的二進位制檔案傳送的帶有新欄位的資料時,這些新欄位在舊的二進位制檔案中就成為未知欄位。

版本訊息保留未知欄位,並在解析和序列化輸出中包含它們,這與 proto2 和 proto3 的行為一致。

保留未知欄位

一些操作可能導致未知欄位丟失。例如,如果你執行以下操作之一,未知欄位將丟失:

  • 將 proto 序列化為 JSON。
  • 遍歷訊息中的所有欄位以填充一個新訊息。

為避免丟失未知欄位,請執行以下操作:

  • 使用二進位制格式;避免使用文字格式進行資料交換。
  • 使用面向訊息的 API,如 CopyFrom()MergeFrom(),來複制資料,而不是逐個欄位複製。

TextFormat 是一個有點特殊的情況。序列化為 TextFormat 會使用欄位編號列印未知欄位。但如果存在使用欄位編號的條目,將 TextFormat 資料解析回二進位制 proto 會失敗。

擴充套件

擴充套件是其容器訊息之外定義的欄位;通常在與容器訊息的 .proto 檔案分開的 .proto 檔案中。

為什麼要使用擴充套件?

使用擴充套件主要有兩個原因

  • 容器訊息的 .proto 檔案將有更少的匯入/依賴項。這可以提高構建時間,打破迴圈依賴,並促進鬆散耦合。擴充套件在這方面非常出色。
  • 允許系統以最小的依賴和協調將資料附加到容器訊息。擴充套件不是一個很好的解決方案,因為欄位編號空間有限以及重用欄位編號的後果。如果您的用例需要大量擴充套件的極低協調性,請考慮使用 Any 訊息型別

示例擴充套件

使用擴充套件是一個兩步過程。首先,在您要擴充套件的訊息(“容器”)中,您必須為擴充套件保留一個欄位編號範圍。然後,在單獨的檔案中,您定義擴充套件欄位本身。

這是一個示例,展示瞭如何向通用 UserContent 訊息新增貓咪影片的擴充套件。

步驟 1:在容器訊息中保留一個擴充套件範圍。

容器訊息必須使用 extensions 關鍵字來保留一個欄位編號範圍供他人使用。最佳實踐是也為計劃新增的特定擴充套件新增一個 declaration。此宣告充當前向宣告,使開發人員更容易發現擴充套件並避免重用欄位編號。

// media/user_content.proto
edition = "2023";

package media;

// A container for user-created content.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    }
  ];
}

此宣告指定將在其他地方定義的擴充套件的欄位編號、完整名稱、型別和基數。

步驟 2:在單獨的檔案中定義擴充套件。

擴充套件本身在另一個 .proto 檔案中定義,該檔案通常側重於特定功能(如貓咪影片)。這避免了從通用容器到特定功能的依賴。

// kittens/video_ext.proto
edition = "2023";

import "media/user_content.proto"; // Imports the container message
import "kittens/video.proto";      // Imports the extension's message type

package kittens;

// This defines the extension field.
extend media.UserContent {
  repeated Video kitten_videos = 126;
}

extend 塊將新的 kitten_videos 欄位連接回 media.UserContent 訊息,使用在容器中保留的欄位編號 126

與具有相同欄位編號、型別和基數的標準欄位相比,擴充套件欄位的 wire-format 編碼沒有區別。因此,只要欄位編號、型別和基數保持不變,將標準欄位移出其容器作為擴充套件或將擴充套件欄位移入其容器訊息作為標準欄位是安全的。

然而,由於擴充套件是在容器訊息之外定義的,因此不會生成專門的訪問器來獲取和設定特定的擴充套件欄位。例如,protobuf 編譯器不會生成 AddKittenVideos()GetKittenVideos() 訪問器。相反,擴充套件是透過引數化函式訪問的,例如:HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()

在 C++ 中,它看起來像這樣

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetRepeatedExtension(kittens::kitten_videos).size());
user_content.GetRepeatedExtension(kittens::kitten_videos)[0];

定義擴充套件範圍

如果您是容器訊息的所有者,則需要為訊息的擴充套件定義一個擴充套件範圍。

分配給擴充套件欄位的欄位編號不能用於標準欄位。

在定義擴充套件範圍後,擴充套件它是安全的。一個好的預設值是分配 1000 個相對較小的數字,並使用擴充套件宣告密集填充該空間

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

在實際擴充套件之前新增擴充套件聲明範圍時,應新增 verification = DECLARATION 以強制此新範圍使用宣告。一旦添加了實際宣告,此佔位符即可刪除。

將現有擴充套件範圍拆分為覆蓋相同總範圍的單獨範圍是安全的。這對於將舊版訊息型別遷移到 擴充套件宣告可能很有必要。例如,在遷移之前,範圍可能定義為

message LegacyMessage {
  extensions 1000 to max;
}

遷移後(拆分範圍)可以為

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

增加起始欄位編號或減少結束欄位編號以移動或縮小擴充套件範圍是不安全的。這些更改可能會使現有擴充套件無效。

對於在 proto 的大多數例項中填充的標準欄位,首選使用欄位編號 1 到 15。不建議將這些編號用於擴充套件。

如果您的編號約定可能涉及擴充套件具有非常大的欄位編號,您可以使用 max 關鍵字指定您的擴充套件範圍達到可能的最大欄位編號

message Foo {
  extensions 1000 to max;
}

max 是 229 - 1,即 536,870,911。

選擇擴充套件編號

擴充套件只是可以在其容器訊息之外指定的欄位。所有關於分配欄位編號的相同規則也適用於擴充套件欄位編號。重用欄位編號的相同後果也適用於重用擴充套件欄位編號。

如果容器訊息使用 擴充套件宣告,則選擇唯一的擴充套件欄位編號很簡單。在定義新擴充套件時,從容器訊息中定義的最高擴充套件範圍中選擇所有其他宣告之上最低的欄位編號。例如,如果容器訊息定義如下

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Container 的下一個擴充套件應該新增一個編號為 525000002 的新宣告。

未經驗證的擴充套件編號分配(不推薦)

容器訊息的所有者可以選擇放棄擴充套件宣告,而採用他們自己的未經驗證的擴充套件編號分配策略。

未經驗證的分配方案使用 protobuf 生態系統之外的機制來在選定的擴充套件範圍內分配擴充套件欄位編號。一個例子可能是使用單體倉庫的提交編號。從 protobuf 編譯器的角度來看,此係統是“未經驗證的”,因為無法檢查擴充套件是否使用了正確獲取的擴充套件欄位編號。

與擴充套件宣告等經過驗證的系統相比,未經驗證的系統的好處是可以在不與容器訊息所有者協調的情況下定義擴充套件。

未經驗證的系統的缺點是 protobuf 編譯器無法保護參與者免於重用擴充套件欄位編號。

不推薦使用未經驗證的擴充套件欄位編號分配策略,因為重用欄位編號的後果會影響訊息的所有擴充套件者(而不僅僅是未遵循建議的開發人員)。如果您的用例需要極低的協調性,請考慮使用 Any 訊息

未經驗證的擴充套件欄位編號分配策略僅限於 1 到 524,999,999 的範圍。欄位編號 525,000,000 及以上只能與擴充套件宣告一起使用。

指定擴充套件型別

擴充套件可以是除 oneofmap 之外的任何欄位型別。

巢狀擴充套件(不推薦)

您可以在另一個訊息的範圍內宣告擴充套件

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    int32 likes_count = 111;
  }
  ...
}

在這種情況下,訪問此擴充套件的 C++ 程式碼是

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

換句話說,唯一的影響是 likes_countpuppies.Photo 的範圍內定義。

這是一個常見的混淆源:宣告巢狀在訊息型別中的 extend意味著外部型別和擴充套件型別之間存在任何關係。特別是,前面的示例意味著 PhotoUserProfile 的某種子類。它只意味著符號 likes_countPhoto 的範圍內宣告;它只是一個靜態成員。

一種常見的模式是將擴充套件定義在擴充套件欄位型別的範圍內——例如,這是一個 media.UserContent 的擴充套件,型別為 puppies.Photo,其中擴充套件作為 Photo 的一部分定義

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    Photo puppy_photo = 127;
  }
  ...
}

然而,沒有要求將具有訊息型別的擴充套件定義在該型別內部。您也可以使用標準定義模式

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  Photo puppy_photo = 127;
}

為了避免混淆,首選這種標準(檔案級)語法。對於不熟悉擴充套件的使用者,巢狀語法常常被誤認為是子類化。

Any

`Any` 訊息型別允許您將訊息用作嵌入型別,而無需它們的 .proto 定義。一個 `Any` 包含一個任意序列化的訊息(作為 `bytes`),以及一個作為該訊息型別的全域性唯一識別符號並解析到該型別的 URL。要使用 `Any` 型別,您需要匯入 `google/protobuf/any.proto`。

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

給定訊息型別的預設型別 URL 是 type.googleapis.com/_packagename_._messagename_

不同的語言實現將支援執行時庫幫助程式以型別安全的方式打包和解包 `Any` 值——例如,在 Java 中,`Any` 型別將有特殊的 `pack()` 和 `unpack()` 訪問器,而在 C++ 中則有 `PackFrom()` 和 `UnpackTo()` 方法:

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

如果您希望將包含的訊息限制為少量型別,並且在將新型別新增到列表之前需要許可權,請考慮使用帶擴充套件宣告擴充套件,而不是 Any 訊息型別。

Oneof

如果您的訊息有許多單一欄位,並且最多隻能同時設定一個欄位,您可以透過使用 oneof 功能來強制此行為並節省記憶體。

Oneof 欄位與單一欄位類似,只是 oneof 中的所有欄位共享記憶體,並且最多隻能同時設定一個欄位。設定 oneof 的任何成員都會自動清除所有其他成員。您可以使用特殊的 case()WhichOneof() 方法(取決於您選擇的語言)檢查 oneof 中設定了哪個值(如果有)。

請注意,如果*設定了多個值,則由 proto 中的順序決定的最後一個設定的值將覆蓋所有先前的值*。

oneof 欄位的欄位編號必須在封閉訊息中是唯一的。

使用 Oneof

要在你的 .proto 檔案中定義一個 oneof,你使用 oneof 關鍵字,後跟你 oneof 的名稱,在本例中是 test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

然後,將 oneof 欄位新增到 oneof 定義中。您可以新增任何型別的欄位,除了 map 欄位和 repeated 欄位。如果您需要向 oneof 新增 repeated 欄位,可以使用包含 repeated 欄位的訊息。

在生成的程式碼中,oneof 欄位具有與常規欄位相同的 getter 和 setter。您還會獲得一個特殊方法,用於檢查 oneof 中設定了哪個值(如果有)。您可以在相關API 參考中找到有關您選擇的語言的 oneof API 的更多資訊。

Oneof 特性

  • 設定一個 oneof 欄位將自動清除 oneof 的所有其他成員。所以如果你設定了幾個 oneof 欄位,只有你設定的*最後一個*欄位仍然有值。

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    // Calling mutable_sub_message() will clear the name field and will set
    // sub_message to a new instance of SubMessage with none of its fields set.
    message.mutable_sub_message();
    CHECK(!message.has_name());
    
  • 如果解析器在傳輸線上遇到同一個 oneof 的多個成員,則在解析後的訊息中只使用看到的最後一個成員。在解析傳輸線上的資料時,從位元組的開頭開始,評估下一個值,並應用以下解析規則:

    • 首先,檢查同一個 oneof 中的*另一個*欄位當前是否已設定,如果是,則清除它。

    • 然後像該欄位不在 oneof 中一樣應用內容:

      • 一個基本型別將覆蓋任何已設定的值。
      • 一個訊息將合併到任何已設定的值中。
  • Oneof 不支援擴充套件。

  • 一個 oneof 不能是 repeated

  • 反射 API 適用於 oneof 欄位。

  • 如果你將一個 oneof 欄位設定為預設值(例如將一個 int32 oneof 欄位設定為 0),該 oneof 欄位的“case”將被設定,並且該值將被序列化到線路上。

  • 如果你正在使用 C++,請確保你的程式碼不會導致記憶體崩潰。下面的示例程式碼會崩潰,因為 sub_message 已經透過呼叫 set_name() 方法被刪除了。

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • 同樣在 C++ 中,如果你 Swap() 兩個帶有 oneofs 的訊息,每個訊息最終會得到對方的 oneof case:在下面的例子中,msg1 將有一個 sub_message,而 msg2 將有一個 name

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

向後相容性問題

在新增或刪除 oneof 欄位時要小心。如果檢查 oneof 的值返回 `None`/`NOT_SET`,這可能意味著 oneof 尚未設定,或者它被設定為 oneof 不同版本中的一個欄位。無法區分這兩種情況,因為無法知道傳輸線上的未知欄位是否是 oneof 的成員。

標籤重用問題

  • 將單一欄位移入或移出 oneof:訊息序列化和解析後,您可能會丟失一些資訊(某些欄位將被清除)。但是,您可以安全地將單個欄位移入新的 oneof,並且如果已知只有一個欄位被設定,則可以移動多個欄位。有關更多詳細資訊,請參閱更新訊息型別
  • 刪除一個 oneof 欄位再加回來:這可能會在訊息被序列化和解析後清除你當前設定的 oneof 欄位。
  • 拆分或合併 oneof:這與移動單一欄位有類似的問題。

對映

如果你想在資料定義中建立關聯對映,protocol buffers 提供了一種方便的快捷語法:

map<key_type, value_type> map_field = N;

...其中 `key_type` 可以是任何整數或字串型別(即,除浮點型別和 `bytes` 之外的任何標量型別)。請注意,列舉和 proto 訊息都不能作為 `key_type`。`value_type` 可以是除另一個 map 之外的任何型別。

因此,舉例來說,如果你想建立一個專案對映,其中每個 Project 訊息都與一個字串鍵相關聯,你可以這樣定義它:

map<string, Project> projects = 3;

對映特性

  • Map 不支援擴充套件。
  • Map 欄位不能是 repeated
  • 對映值的線路格式順序和對映迭代順序是未定義的,所以你不能依賴你的對映項會以特定的順序排列。
  • .proto 生成文字格式時,對映按鍵排序。數字鍵按數值排序。
  • 從線路解析或合併時,如果存在重複的對映鍵,則使用最後看到的鍵。從文字格式解析對映時,如果存在重複的鍵,解析可能會失敗。
  • 如果你為一個 map 欄位提供了鍵但沒有提供值,該欄位被序列化時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,會序列化該型別的預設值,而在其他語言中則什麼也不序列化。
  • 符號 FooEntry 不能與 map foo 存在於同一作用域,因為 FooEntry 已經被 map 的實現使用了。

生成的 map API 目前可用於所有支援的語言。你可以在相關的 API 參考中找到更多關於你所選語言的 map API 的資訊。

向後相容性

map 語法線上路上等同於以下內容,因此不支援 map 的 protocol buffers 實現仍然可以處理你的資料:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支援對映的 protocol buffers 實現都必須既能生成也能接受可被先前定義接受的資料。

包(Packages)

你可以向 .proto 檔案新增一個可選的 package 說明符,以防止協議訊息型別之間的名稱衝突。

package foo.bar;
message Open { ... }

然後你可以在定義你的訊息型別的欄位時使用包說明符:

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

包說明符影響生成程式碼的方式取決於你選擇的語言:

  • C++ 中,生成的類被包裹在一個 C++ 名稱空間內。例如,Open 會在名稱空間 foo::bar 中。
  • JavaKotlin 中,除非你在你的 .proto 檔案中明確提供一個 option java_package,否則該包將用作 Java 包。
  • Python 中,package 指令被忽略,因為 Python 模組是根據它們在檔案系統中的位置來組織的。
  • Go 中,`package` 指令被忽略,生成的 `.pb.go` 檔案位於以相應 `go_proto_library` Bazel 規則命名的包中。對於開源專案,您**必須**提供一個 `go_package` 選項或設定 Bazel `-M` 標誌。
  • Ruby 中,生成的類被包裝在巢狀的 Ruby 名稱空間內,並轉換為所需的 Ruby 大寫風格(首字母大寫;如果第一個字元不是字母,則字首為 `PB_`)。例如,`Open` 將位於 `Foo::Bar` 名稱空間中。
  • PHP 中,除非您在 .proto 檔案中明確提供了 `option php_namespace`,否則 package 在轉換為 PascalCase 後被用作名稱空間。例如,`Open` 將位於 `Foo\Bar` 名稱空間中。
  • C# 中,除非您在 `.proto` 檔案中明確提供 `option csharp_namespace`,否則 package 在轉換為 PascalCase 後將用作名稱空間。例如,`Open` 將位於 `Foo.Bar` 名稱空間中。

請注意,即使 `package` 指令不直接影響生成的程式碼(例如在 Python 中),仍然強烈建議為 .proto 檔案指定包,否則可能導致描述符中的命名衝突,並使 proto 對其他語言不可移植。

包和名稱解析

protocol buffer 語言中的型別名稱解析方式類似於 C++:首先搜尋最內層的作用域,然後是次內層,以此類推,每個包都被視為其父包的“內層”。前導的點“.”(例如,`.foo.bar.Baz`)表示從最外層作用域開始。

protocol buffer 編譯器透過解析匯入的 .proto 檔案來解析所有型別名稱。每種語言的程式碼生成器都知道如何在該語言中引用每種型別,即使它有不同的作用域規則。

定義服務

如果您想將您的訊息型別與 RPC(遠端過程呼叫)系統一起使用,您可以在 .proto 檔案中定義一個 RPC 服務介面,protocol buffer 編譯器將以您選擇的語言生成服務介面程式碼和存根 (stub)。因此,舉例來說,如果您想定義一個 RPC 服務,其方法接受您的 SearchRequest 並返回一個 SearchResponse,您可以在您的 .proto 檔案中這樣定義:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

與協議緩衝區一起使用的最直接的 RPC 系統是 gRPC:一個由 Google 開發的語言和平臺中立的開源 RPC 系統。gRPC 與協議緩衝區配合得非常好,允許您使用特殊的協議緩衝區編譯器外掛直接從 .proto 檔案生成相關的 RPC 程式碼。

如果您不想使用 gRPC,也可以將協議緩衝區與您自己的 RPC 實現一起使用。您可以在 Proto2 語言指南中找到更多資訊。

還有一些正在進行的第三方專案,用於開發 Protocol Buffers 的 RPC 實現。有關我們所知道的專案連結列表,請參閱 第三方附加元件 Wiki 頁面

JSON 對映

標準 protobuf 二進位制有線格式是使用 protobuf 的兩個系統之間通訊的首選序列化格式。對於與使用 JSON 而不是 protobuf 有線格式的系統進行通訊,Protobuf 支援 ProtoJSON 中的規範編碼。

選項

.proto 檔案中的單個宣告可以用許多*選項*來註解。選項不會改變宣告的整體含義,但可能會影響它在特定上下文中的處理方式。可用選項的完整列表定義在 /google/protobuf/descriptor.proto 中。

一些選項是檔案級選項,意味著它們應該寫在頂層作用域,而不是任何訊息、列舉或服務定義內部。一些選項是訊息級選項,意味著它們應該寫在訊息定義內部。一些選項是欄位級選項,意味著它們應該寫在欄位定義內部。選項也可以寫在列舉型別、列舉值、oneof 欄位、服務型別和服務方法上;然而,目前對於這些都沒有任何有用的選項。

以下是一些最常用的選項:

  • java_package (檔案選項): 您希望用於生成的 Java/Kotlin 類的包名。如果在 .proto 檔案中沒有明確給出 java_package 選項,那麼預設情況下將使用 proto 包(在 .proto 檔案中使用“package”關鍵字指定)。然而,proto 包通常不是好的 Java 包,因為 proto 包不要求以反向域名開頭。如果不生成 Java 或 Kotlin 程式碼,此選項無效。

    option java_package = "com.example.foo";
    
  • java_outer_classname (檔案選項): 您想要生成的包裝器 Java 類的類名(因此也是檔名)。如果在 .proto 檔案中沒有明確指定 java_outer_classname,類名將透過將 .proto 檔名轉換為駝峰式來構造(所以 foo_bar.proto 變為 FooBar.java)。如果 java_multiple_files 選項被停用,那麼為 .proto 檔案生成的所有其他類/列舉等都將作為巢狀類/列舉等生成在這個外部包裝器 Java 類*內部*。如果不生成 Java 程式碼,此選項無效。

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files(檔案選項):如果為 false,則此 .proto 檔案只生成一個 .java 檔案,並且為頂級訊息、服務和列舉生成的所有 Java 類/列舉等將巢狀在外部類中(參見 java_outer_classname)。如果為 true,則為頂級訊息、服務和列舉生成的每個 Java 類/列舉等都將生成單獨的 .java 檔案,並且為本 .proto 檔案生成的包裝 Java 類將不包含任何巢狀類/列舉等。這是一個布林選項,預設為 false。如果不生成 Java 程式碼,此選項無效。此選項已在 2024 版中刪除,並替換為 features.(pb.java).nest_in_file_class

    option java_multiple_files = true;
    
  • optimize_for(檔案選項):可以設定為 SPEEDCODE_SIZELITE_RUNTIME。這會以下列方式影響 C++ 和 Java 程式碼生成器(以及可能的第三方生成器):

    • SPEED (預設): protocol buffer 編譯器將生成用於序列化、解析以及對你的訊息型別執行其他常見操作的程式碼。此程式碼經過高度最佳化。
    • CODE_SIZE:protocol buffer 編譯器將生成最小化的類,並依賴於共享的、基於反射的程式碼來實現序列化、解析和各種其他操作。因此,生成的程式碼將比使用 `SPEED` 時小得多,但操作會更慢。類仍然會實現與 `SPEED` 模式下完全相同的公共 API。此模式在包含大量 .proto 檔案且不需要所有檔案都快如閃電的應用中最為有用。
    • LITE_RUNTIME:protocol buffer 編譯器將生成僅依賴於“lite”執行時庫(libprotobuf-lite 而不是 libprotobuf)的類。lite 執行時比完整庫小得多(大約小一個數量級),但省略了某些功能,如描述符和反射。這對於在受限平臺(如手機)上執行的應用特別有用。編譯器仍將像在 `SPEED` 模式下一樣生成所有方法的快速實現。生成的類在每種語言中只會實現 `MessageLite` 介面,該介面僅提供完整 `Message` 介面方法的一個子集。
    option optimize_for = CODE_SIZE;
    
  • cc_generic_services, java_generic_services, py_generic_services (檔案選項): 通用服務已被棄用。 protocol buffer 編譯器是否應分別基於 C++、Java 和 Python 中的服務定義生成抽象服務程式碼。由於歷史原因,這些預設為 `true`。然而,自 2.3.0 版本(2010年1月)起,RPC 實現更傾向於提供程式碼生成器外掛來生成更特定於每個系統的程式碼,而不是依賴於“抽象”服務。

    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas (檔案選項): 為 C++ 生成的程式碼啟用 arena 分配

  • objc_class_prefix (檔案選項): 設定 Objective-C 類字首,該字首會新增到此 .proto 生成的所有 Objective-C 類和列舉之前。沒有預設值。您應該使用 3-5 個大寫字元的字首,正如 Apple 推薦的那樣。請注意,所有 2 個字母的字首都由 Apple 保留。

  • packed(欄位選項):在 protobuf 版本中,此選項被鎖定為 true。要使用非打包有線格式,您可以使用版本特性覆蓋此選項。這提供了與 2.3.0 版本之前的解析器(很少需要)的相容性,示例如下

    repeated int32 samples = 4 [features.repeated_field_encoding = EXPANDED];
    
  • deprecated (欄位選項): 如果設定為 `true`,表示該欄位已棄用,新程式碼不應使用。在大多數語言中,這沒有實際效果。在 Java 中,它會變成一個 `@Deprecated` 註解。對於 C++,clang-tidy 將在使用已棄用欄位時生成警告。將來,其他特定語言的程式碼生成器可能會在該欄位的訪問器上生成棄用註解,這反過來又會在編譯試圖使用該欄位的程式碼時發出警告。如果該欄位無人使用,並且您想阻止新使用者使用它,請考慮用保留語句替換該欄位宣告。

    int32 old_field = 6 [deprecated = true];
    

列舉值選項

支援列舉值選項。你可以使用 deprecated 選項來指示某個值不應再使用。你還可以使用擴充套件建立自定義選項。

下面的例子展示了新增這些選項的語法:

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

讀取 string_name 選項的 C++ 程式碼可能看起來像這樣:

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

請參閱自定義選項,瞭解如何將自定義選項應用於列舉值和欄位。

自定義選項

Protocol Buffers 還允許您定義和使用自己的選項。請注意,這是一個高階功能,大多數人不需要。如果您確實認為需要建立自己的選項,請參閱 Proto2 語言指南以獲取詳細資訊。請注意,建立自定義選項會使用 擴充套件

從 2024 版開始,使用 import option 匯入自定義選項定義。請參閱 匯入

選項保留

選項有一個*保留期 (retention)* 的概念,它控制一個選項是否在生成的程式碼中被保留。選項預設具有*執行時保留期*,意味著它們在生成的程式碼中被保留,因此在執行時在生成的描述符池中是可見的。但是,您可以設定 `retention = RETENTION_SOURCE` 來指定一個選項(或選項中的欄位)在執行時不得被保留。這被稱為*原始碼保留期*。

選項保留是一個高階功能,大多數使用者不需要擔心,但如果您想使用某些選項而不想為在二進位制檔案中保留它們付出程式碼大小的代價,它可能很有用。具有源保留的選項對於 `protoc` 和 `protoc` 外掛仍然可見,因此程式碼生成器可以使用它們來自定義其行為。

保留可以直接在選項上設定,如下所示:

extend google.protobuf.FileOptions {
  int32 source_retention_option = 1234
      [retention = RETENTION_SOURCE];
}

它也可以設定在一個普通欄位上,在這種情況下,它僅在該欄位出現在選項內部時生效:

message OptionsMessage {
  int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}

您可以設定 `retention = RETENTION_RUNTIME`,但這沒有效果,因為這是預設行為。當一個訊息欄位被標記為 `RETENTION_SOURCE` 時,它的全部內容都會被丟棄;它內部的欄位不能透過嘗試設定 `RETENTION_RUNTIME` 來覆蓋這一點。

選專案標

欄位有一個 `targets` 選項,它控制該欄位在用作選項時可以應用於的實體型別。例如,如果一個欄位有 `targets = TARGET_TYPE_MESSAGE`,那麼該欄位就不能在列舉(或任何其他非訊息實體)的自定義選項中設定。Protoc 會強制執行這一點,並且如果違反目標約束,會引發錯誤。

乍一看,這個功能似乎沒有必要,因為每個自定義選項都是特定實體選項訊息的擴充套件,這已經將選項限制在該一個實體上。然而,在您有一個應用於多種實體型別的共享選項訊息,並且您想控制該訊息中各個欄位的用法時,選專案標就很有用了。例如:

message MyOptions {
  string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                     targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  MyOptions enum_options = 50000;
}

// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: this field is allowed on both message and enum options
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Error: file_only_option cannot be set on an enum.
  option (enum_options).file_only_option = "xyz";
}

生成你的類

要生成您需要使用的 Java、Kotlin、Python、C++、Go、Ruby、Objective-C 或 C# 程式碼來處理在 .proto 檔案中定義的訊息型別,您需要在 .proto 檔案上執行 protocol buffer 編譯器 protoc。如果您還沒有安裝編譯器,下載軟體包並按照 README 中的說明進行操作。對於 Go,您還需要為編譯器安裝一個特殊的程式碼生成器外掛;您可以在 GitHub 的 golang/protobuf 倉庫中找到它和安裝說明。

協議編譯器的呼叫方式如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH 指定在解析 import 指令時查詢 .proto 檔案的目錄。如果省略,則使用當前目錄。可以透過多次傳遞 --proto_path 選項來指定多個匯入目錄;它們將按順序搜尋。-I=_IMPORT_PATH_ 可以用作 --proto_path 的簡寫形式。

注意: 相對於其 `proto_path` 的檔案路徑在給定的二進位制檔案中必須是全域性唯一的。例如,如果您有 `proto/lib1/data.proto` 和 `proto/lib2/data.proto`,這兩個檔案不能與 `-I=proto/lib1 -I=proto/lib2` 一起使用,因為 `import "data.proto"` 的含義將不明確。相反,應該使用 `-Iproto/`,全域性名稱將是 `lib1/data.proto` 和 `lib2/data.proto`。

如果您正在釋出一個庫,並且其他使用者可能會直接使用您的訊息,您應該在他們預期使用的路徑中包含一個唯一的庫名,以避免檔名衝突。如果您在一個專案中有多個目錄,最好的做法是傾向於將一個 `-I` 設定為專案的頂層目錄。

  • 你可以提供一個或多個輸出指令

    為了方便起見,如果 `DST_DIR` 以 `.zip` 或 `.jar` 結尾,編譯器會將輸出寫入一個具有給定名稱的 ZIP 格式的存檔檔案中。`.jar` 輸出也會被賦予一個 Java JAR 規範所要求的清單檔案。請注意,如果輸出存檔已經存在,它將被覆蓋。

  • 您必須提供一個或多個 .proto 檔案作為輸入。可以一次指定多個 .proto 檔案。儘管檔案是相對於當前目錄命名的,但每個檔案必須位於其中一個 IMPORT_PATH 中,以便編譯器可以確定其規範名稱。

檔案位置

最好不要將 .proto 檔案與其他語言的原始檔放在同一個目錄中。考慮在你的專案根包下為 .proto 檔案建立一個子包 proto

位置應與語言無關

在處理 Java 程式碼時,將相關的 `.proto` 檔案放在與 Java 原始碼相同的目錄中很方便。但是,如果任何非 Java 程式碼使用相同的 proto,路徑字首將不再有意義。因此,通常應將 proto 放在與語言無關的相關目錄中,例如 `//myteam/mypackage`。

這條規則的例外是當很明顯 protos 只會在 Java 上下文中使用時,例如用於測試。

支援的平臺

有關資訊