語言指南 (proto 3)

涵蓋如何在您的專案中使用 Protocol Buffers 語言的 proto3 修訂版本。

本指南說明如何使用 Protocol Buffer 語言來建構您的 Protocol Buffer 資料,包括 .proto 檔案語法,以及如何從您的 .proto 檔案產生資料存取類別。本指南涵蓋 Protocol Buffer 語言的 proto3 修訂版本。

如需 版本 語法的相關資訊,請參閱Protobuf 版本語言指南

如需 proto2 語法的相關資訊,請參閱Proto2 語言指南

這是參考指南,如需逐步範例,其中使用本文件中說明的許多功能,請參閱您選擇的語言的教學課程

定義訊息類型

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

syntax = "proto3";

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

    • edition (或 proto2/proto3 的 syntax) 必須是檔案中第一個非空白、非註解的行。
    • 如果未指定 editionsyntax,Protocol Buffer 編譯器會假設您正在使用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 線路格式很精簡,並且沒有提供一種方法來偵測使用一個定義編碼並使用另一個定義解碼的欄位。

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

  • 開發人員時間浪費在偵錯上
  • 剖析/合併錯誤 (最好的情況)
  • PII/SPII 洩漏
  • 資料損毀

欄位編號重複使用的常見原因

  • 重新編號欄位 (有時是為了實現更美觀的欄位編號順序而完成)。重新編號實際上會刪除並重新新增重新編號中涉及的所有欄位,從而導致不相容的線路格式變更。
  • 刪除欄位且未保留編號以防止未來重複使用。

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

指定欄位基數

訊息欄位可以是下列其中一種

  • 單數:

    在 proto3 中,有兩種單數欄位類型

    • optional:(建議) optional 欄位有兩種可能的狀態之一

      • 欄位已設定,並且包含從線路明確設定或剖析的值。它將序列化到線路上。
      • 欄位未設定,並且將傳回預設值。它不會序列化到線路上。

      您可以檢查以查看值是否已明確設定。

      為了與 protobuf 版本和 proto2 最大相容性,建議使用 optional 而不是隱含欄位。

    • 隱含:(不建議) 隱含欄位沒有明確的基數標籤,其行為如下

      • 如果欄位是訊息類型,則其行為就像 optional 欄位一樣。

      • 如果欄位不是訊息,則它有兩種狀態

        • 欄位設定為從線路明確設定或剖析的非預設 (非零) 值。它將序列化到線路上。
        • 欄位設定為預設 (零) 值。它不會序列化到線路上。事實上,您無法判斷預設 (零) 值是從線路設定或剖析,還是根本未提供。如需有關此主題的更多資訊,請參閱欄位存在性
  • repeated:此欄位類型可以在格式正確的訊息中重複零次或多次。重複值的順序將被保留。

  • map:這是一種成對的鍵/值欄位類型。請參閱Map,瞭解有關此欄位類型的更多資訊。

重複欄位預設為封裝

在 proto3 中,純量數值類型的 repeated 欄位預設使用 packed 編碼。

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

訊息類型欄位始終具有欄位存在性

在 proto3 中,訊息類型欄位已經具有欄位存在性。因此,新增 optional 修飾詞不會變更欄位的欄位存在性。

以下程式碼範例中 Message2Message3 的定義會為所有語言產生相同的程式碼,並且在二進位、JSON 和 TextFormat 中的表示形式沒有差異

syntax="proto3";

package foo.bar;

message Message1 {}

message Message2 {
  Message1 foo = 1;
}

message Message3 {
  optional Message1 bar = 1;
}

格式正確的訊息

術語「格式正確」在應用於 protobuf 訊息時,指的是序列化/還原序列化的位元組。protoc 剖析器會驗證給定的 proto 定義檔案是否可剖析。

單數欄位可以在線路格式位元組中多次出現。剖析器將接受輸入,但只有該欄位的最後一個實例可以透過產生的繫結存取。請參閱最後一個獲勝,瞭解有關此主題的更多資訊。

新增更多訊息類型

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

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

message SearchResponse {
 ...
}

合併訊息會導致膨脹 雖然多個訊息類型 (例如訊息、列舉和服務) 可以在單個 .proto 檔案中定義,但當大量具有不同相依性的訊息在單個檔案中定義時,也可能導致相依性膨脹。建議每個 .proto 檔案包含的訊息類型盡可能少。

新增註解

若要在您的 .proto 檔案中新增註解

  • 建議在 .proto 程式碼元素之前的行上使用 C/C++/Java 行尾樣式註解「//」

  • 也接受 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、可 Null 欄位存取器和複製函數。
  • Python 有點不同,Python 編譯器會產生一個模組,其中包含您的 .proto 中每個訊息類型的靜態描述器,然後該模組會與元類別一起使用,以在執行階段建立必要的 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
stringstd::stringStringstr/unicode[5]stringString (UTF-8)stringstringStringProtoString
bytesstd::stringByteStringstr (Python 2), bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin 使用 Java 中的對應類型,即使對於無號類型也是如此,以確保混合 Java/Kotlin 程式碼庫中的相容性。

[2] 在 Java 中,無號 32 位元和 64 位元整數使用其帶號對應項表示,最高位元只是儲存在符號位元中。

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

[4] 64 位元或無號 32 位元整數在解碼時始終表示為 long,但如果在設定欄位時給定 int,則可以是 int。在所有情況下,值都必須適合設定時表示的類型。請參閱 [2]。

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

[6] 整數用於 64 位元機器,字串用於 32 位元機器。

您可以在Protocol Buffer 編碼中找到更多有關在序列化訊息時如何編碼這些類型的資訊。

預設欄位值

當剖析訊息時,如果編碼的訊息位元組不包含特定欄位,則存取剖析物件中的該欄位會傳回該欄位的預設值。預設值是類型特定的

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

重複欄位的預設值為空 (通常是適當語言中的空清單)。

map 欄位的預設值為空 (通常是適當語言中的空 map)。

請注意,對於隱含存在純量欄位,一旦剖析訊息,就無法判斷該欄位是否已明確設定為預設值 (例如,布林值是否已設定為 false),或者根本未設定:在定義訊息類型時,您應該記住這一點。例如,如果您不希望預設情況下也發生這種行為,請不要讓布林值在設定為 false 時開啟某些行為。另請注意,如果純量訊息欄位設定為其預設值,則該值不會序列化到線路上。如果浮點數或雙精度浮點數值設定為 +0,則不會序列化,但 -0 被視為不同且會序列化。

請參閱您選擇的語言的產生的程式碼指南,瞭解有關預設值如何在產生的程式碼中運作的更多詳細資訊。

列舉

當您定義訊息類型時,您可能希望其欄位之一僅具有預定義值清單中的一個值。例如,假設您想要為每個 SearchRequest 新增一個 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,因為這是列舉中定義的第一個值。

在 proto3 中,列舉定義中定義的第一個值必須具有值零,並且應具有名稱 ENUM_TYPE_NAME_UNSPECIFIEDENUM_TYPE_NAME_UNKNOWN。這是因為

  • 必須有一個零值,以便我們可以將 0 用作數值預設值
  • 零值需要是第一個元素,以便與proto2 語意相容,在 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) 的語言中,列舉中的一個案例用於表示無法辨識的值,並且可以使用特殊的存取器存取基礎整數。在任一種情況下,如果訊息已序列化,則無法辨識的值仍將與訊息一起序列化。

如需有關如何在您的應用程式中使用訊息 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 "myproject/other_protos.proto";

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

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

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

// 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

Protocol 編譯器會在 Protocol 編譯器命令列中使用 -I/--proto_path 旗標指定的一組目錄中搜尋匯入的檔案。如果未給定旗標,它會在調用編譯器的目錄中尋找。一般來說,您應該將 --proto_path 旗標設定為專案的根目錄,並對所有匯入使用完整限定名稱。

使用 proto2 訊息類型

可以匯入proto2 訊息類型,並在您的 proto3 訊息中使用它們,反之亦然。但是,proto2 列舉不能直接在 proto3 語法中使用 (如果匯入的 proto2 訊息使用它們,則沒問題)。

巢狀類型

您可以在其他訊息類型內定義和使用訊息類型,如下列範例所示 – 此處的 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 最佳實務和以下規則

  • 請勿變更任何現有欄位的欄位編號。「變更」欄位編號相當於刪除該欄位並新增一個具有相同類型的新欄位。如果您想重新編號欄位,請參閱刪除欄位的說明。
  • 如果您新增新欄位,任何使用「舊」訊息格式的程式碼序列化的訊息仍然可以由您新產生的程式碼解析。您應該記住這些元素的預設值,以便新程式碼可以正確地與舊程式碼產生的訊息互動。同樣地,您的新程式碼建立的訊息可以由您的舊程式碼解析:舊的二進位檔在解析時只會忽略新的欄位。有關詳細資訊,請參閱未知欄位章節。
  • 欄位可以被移除,只要該欄位編號在您更新的訊息類型中不再使用即可。您可能想要重新命名該欄位,或許新增前綴「OBSOLETE_」,或將欄位編號保留,以便您 .proto 的未來使用者不會意外地重複使用該編號。
  • int32uint32int64uint64bool 都是相容的 – 這表示您可以將欄位從這些類型之一變更為另一個,而不會破壞向前或向後相容性。如果從線路解析的數字不符合對應的類型,您將獲得與在 C++ 中將數字轉換為該類型相同的效果(例如,如果將 64 位元數字讀取為 int32,它將被截斷為 32 位元)。
  • sint32sint64 彼此相容,但與其他整數類型相容。
  • stringbytes 只要位元組是有效的 UTF-8 就相容。
  • 嵌入式訊息與 bytes 相容,如果位元組包含訊息的編碼實例。
  • fixed32sfixed32 相容,fixed64sfixed64 相容。
  • 對於 stringbytes 和訊息欄位,單數與 repeated 相容。給定重複欄位的序列化資料作為輸入,期望此欄位為單數的用戶端將採用最後一個輸入值(如果是原始類型欄位),或合併所有輸入元素(如果是訊息類型欄位)。請注意,這對於數值類型(包括布林值和列舉)通常是安全的。數值類型的重複欄位預設以封裝格式序列化,當期望單數欄位時,將無法正確解析。
  • enum 在線路格式方面與 int32uint32int64uint64 相容(請注意,如果值不符合,則會被截斷)。但是,請注意,當訊息被還原序列化時,用戶端程式碼可能會以不同的方式處理它們:例如,無法識別的 proto3 enum 值將保留在訊息中,但當訊息被還原序列化時,這如何表示取決於語言。整數欄位始終只保留其值。
  • 將單個 optional 欄位或擴充功能變更為新的 oneof 的成員是二進位相容的,但是對於某些語言(特別是 Go),產生的程式碼的 API 將以不相容的方式變更。因此,Google 不會在其公共 API 中進行此類變更,如 AIP-180 中所述。對於來源相容性的相同警告,如果您確定沒有程式碼一次設定多個欄位,則將多個欄位移至新的 oneof 可能安全。將欄位移至現有的 oneof 是不安全的。同樣地,將單個欄位 oneof 變更為 optional 欄位或擴充功能是安全的。
  • map<K, V> 和對應的 repeated 訊息欄位之間變更欄位是二進位相容的(有關訊息佈局和其他限制,請參閱下方的映射)。但是,變更的安全性取決於應用程式:當還原序列化和重新序列化訊息時,使用 repeated 欄位定義的用戶端將產生語義上相同的結果;但是,使用 map 欄位定義的用戶端可能會重新排序條目並刪除具有重複鍵的條目。

不明欄位

未知欄位是格式正確的 Protocol Buffer 序列化資料,表示解析器無法識別的欄位。例如,當舊的二進位檔解析由具有新欄位的新二進位檔傳送的資料時,這些新欄位會變成舊二進位檔中的未知欄位。

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

保留不明欄位

某些動作可能會導致未知欄位遺失。例如,如果您執行以下其中一項操作,則未知欄位會遺失

  • 將 proto 序列化為 JSON。
  • 迭代訊息中的所有欄位以填入新訊息。

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

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

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

Any

Any 訊息類型可讓您使用訊息作為嵌入式類型,而無需擁有其 .proto 定義。Any 包含任意序列化的訊息作為 bytes,以及一個 URL,該 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 ...
  }
}

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,您可以使用包含重複欄位的訊息。

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

Oneof 功能

  • 設定 oneof 欄位將自動清除 oneof 的所有其他成員。因此,如果您設定了多個 oneof 欄位,則只有您設定的最後一個欄位仍然具有值。

    SampleMessage message;
    message.set_name("name");
    CHECK_EQ(message.name(), "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.name().empty());
    
  • 如果解析器在線路上遇到同一個 oneof 的多個成員,則在解析的訊息中僅使用最後看到的成員。當解析線路上的資料時,從位元組的開頭開始,評估下一個值,並套用以下解析規則

    • 首先,檢查是否目前設定了同一個 oneof 中的不同欄位,如果是,則清除它。

    • 然後套用內容,如同該欄位不在 oneof 中一樣

      • 原始類型將覆寫任何已設定的值
      • 訊息將合併到任何已設定的值中
  • Oneof 不能是 repeated

  • Reflection 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() 具有 oneof 的兩個訊息,則每個訊息都將以另一個訊息的 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_EQ(msg2.name(), "name");
    

回溯相容性問題

在新增或移除 oneof 欄位時請小心。如果檢查 oneof 的值傳回 None/NOT_SET,則可能表示 oneof 尚未設定,或者已設定為不同版本的 oneof 中的欄位。沒有辦法分辨差異,因為無法知道線路上的未知欄位是否為 oneof 的成員。

標籤重複使用問題

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

Map

如果您想建立關聯式映射作為資料定義的一部分,Protocol Buffer 提供了方便的快捷方式語法

map<key_type, value_type> map_field = N;

…其中 key_type 可以是任何整數或字串類型(因此,除了浮點類型和 bytes 之外的任何純量類型)。請注意,列舉和 proto 訊息都對 key_type 無效。value_type 可以是除另一個映射之外的任何類型。

因此,例如,如果您想建立專案映射,其中每個 Project 訊息都與字串鍵相關聯,您可以這樣定義它

map<string, Project> projects = 3;

Map 功能

  • 映射欄位不能是 repeated
  • 映射值的線路格式排序和映射迭代排序是未定義的,因此您不能依賴映射項目以特定順序排列。
  • 當為 .proto 產生文字格式時,映射會按鍵排序。數值鍵會按數值排序。
  • 當從線路解析或合併時,如果有重複的映射鍵,則使用最後看到的鍵。當從文字格式解析映射時,如果存在重複的鍵,解析可能會失敗。
  • 如果您為映射欄位提供鍵但沒有值,則序列化欄位時的行為取決於語言。在 C++、Java、Kotlin 和 Python 中,該類型的預設值會被序列化,而在其他語言中則不會序列化任何內容。
  • 符號 FooEntry 不能與映射 foo 存在於相同的範圍中,因為 FooEntry 已被映射的實作所使用。

產生的映射 API 目前適用於所有支援的語言。您可以在相關的API 參考中找到有關您選擇的語言的映射 API 的更多資訊。

回溯相容性

映射語法在線路上等效於以下內容,因此不支援映射的 Protocol Buffer 實作仍然可以處理您的資料

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

repeated MapFieldEntry map_field = N;

任何支援映射的 Protocol Buffer 實作都必須產生和接受可以被早期定義接受的資料。

套件

您可以在 .proto 檔案中新增可選的 package 指定符,以防止 Protocol 訊息類型之間的名稱衝突。

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 中,套件在轉換為 PascalCase 後用作命名空間,除非您明確提供 option php_namespace 在您的 .proto 檔案中。例如,Open 將在命名空間 Foo\Bar 中。
  • C# 中,套件在轉換為 PascalCase 後用作命名空間,除非您明確提供 option csharp_namespace 在您的 .proto 檔案中。例如,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);
}

與 Protocol Buffer 一起使用的最直接的 RPC 系統是 gRPC:Google 開發的與語言和平台無關的開放原始碼 RPC 系統。gRPC 與 Protocol Buffer 特別搭配使用,並讓您使用特殊的 Protocol Buffer 編譯器外掛程式直接從您的 .proto 檔案產生相關的 RPC 程式碼。

如果您不想使用 gRPC,也可以將 Protocol Buffer 與您自己的 RPC 實作一起使用。您可以在Proto2 語言指南中找到有關此內容的更多資訊。

還有許多正在進行的第三方專案,旨在開發 Protocol Buffer 的 RPC 實作。如需我們所知專案的連結列表,請參閱第三方附加元件 Wiki 頁面

JSON 對應

標準的 protobuf 二進位線路格式是用於使用 protobuf 的兩個系統之間通訊的首選序列化格式。為了與使用 JSON 而不是 protobuf 線路格式的系統通訊,Protobuf 支援 JSON 中的標準編碼。

選項

.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 程式碼,則此選項無效。

    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_servicesjava_generic_servicespy_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(欄位選項):在基本數值類型的重複欄位上預設為 true,導致使用更緊湊的編碼。若要使用未封裝的線路格式,可以將其設定為 false。這提供了與 2.3.0 版之前的解析器(很少需要)的相容性,如下列範例所示

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

    int32 old_field = 6 [deprecated = true];
    

列舉值選項

支援列舉值選項。您可以使用 deprecated 選項來指示不應再使用某個值。您也可以使用擴充功能建立自訂選項。

以下範例顯示新增這些選項的語法

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional 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 Buffer 也允許您定義和使用自己的選項。請注意,這是一個進階功能,大多數人不需要。如果您確實認為您需要建立自己的選項,請參閱Proto2 語言指南以取得詳細資訊。請注意,建立自訂選項會使用 擴充功能,這僅允許用於 proto3 中的自訂選項。

選項保留

選項具有保留的概念,該概念控制選項是否保留在產生的程式碼中。選項預設具有執行階段保留,表示它們保留在產生的程式碼中,因此在產生的描述符池中在執行階段可見。但是,您可以設定 retention = RETENTION_SOURCE 以指定選項(或選項內的欄位)不得在執行階段保留。這稱為來源保留

選項保留是一個進階功能,大多數使用者不必擔心,但如果您想使用某些選項而不支付將它們保留在二進位檔中的程式碼大小成本,它可能會很有用。具有來源保留的選項對於 protocprotoc 外掛程式仍然可見,因此程式碼產生器可以使用它們來自訂其行為。

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

extend google.protobuf.FileOptions {
  optional 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 {
  optional MyOptions file_options = 50000;
}

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

extend google.protobuf.EnumOptions {
  optional 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# 程式碼,您需要使用 Protocol Buffer 編譯器 protoc.proto 檔案上執行,才能使用 .proto 檔案中定義的訊息類型。如果您尚未安裝編譯器,請下載套件並按照 README 中的說明進行操作。對於 Go,您還需要為編譯器安裝特殊的程式碼產生器外掛程式;您可以在 GitHub 上的 golang/protobuf 儲存庫中找到此外掛程式和安裝說明。

Protocol 編譯器的調用方式如下

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 的簡短形式。

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

    作為額外的便利,如果 DST_DIR.zip.jar 結尾,則編譯器會將輸出寫入具有給定名稱的單個 ZIP 格式封存檔。.jar 輸出也將被賦予 Java JAR 規範要求的 Manifest 檔案。請注意,如果輸出封存檔已存在,它將被覆寫。

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

檔案位置

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

位置應與語言無關

使用 Java 程式碼時,將相關的 .proto 檔案放在與 Java 來源相同的目錄中很方便。但是,如果任何非 Java 程式碼曾經使用相同的 proto,則路徑前綴將不再有意義。因此,一般來說,將 proto 放在相關的語言無關目錄中,例如 //myteam/mypackage

此規則的例外情況是,當明確 proto 僅在 Java 環境中使用時,例如用於測試。

支援的平台

有關以下資訊