Proto 最佳實務

分享經審核的 Protocol Buffers 編寫最佳實務。

用戶端和伺服器永遠不會在完全相同的時間更新 - 即使您嘗試在同一時間更新它們。其中一方可能會被回溯。不要假設您可以進行破壞性變更,因為用戶端和伺服器是同步的,所以不會有問題。

不要 重複使用標籤編號

永遠不要重複使用標籤編號。它會搞砸反序列化。即使您認為沒有人正在使用該欄位,也請不要重複使用標籤編號。如果該變更曾經生效,則您的 proto 的序列化版本可能會在某處的記錄中。或者,在另一個伺服器中可能有舊程式碼會中斷。

為已刪除的欄位保留標籤編號

當您刪除不再使用的欄位時,請保留其標籤編號,以便將來沒有人會意外地重複使用它。只需要 reserved 2, 3; 就足夠了。不需要類型(可讓您修剪相依性!)。您也可以保留名稱,以避免重複使用現在已刪除的欄位名稱:reserved "foo", "bar";

為已刪除的列舉值保留編號

當您刪除不再使用的列舉值時,請保留其編號,以便將來沒有人會意外地重複使用它。只需要 reserved 2, 3; 就足夠了。您也可以保留名稱,以避免重複使用現在已刪除的值名稱:reserved "FOO", "BAR";

不要 變更欄位的類型

幾乎永遠不要變更欄位的類型;它會搞砸反序列化,就像重複使用標籤編號一樣。protobuf 文件概述了少數可以接受的情況(例如,在 int32uint32int64bool 之間轉換)。但是,變更欄位的訊息類型將會中斷,除非新訊息是舊訊息的超集合。

不要 加入必要的欄位

永遠不要加入必要的欄位,而是加入 // required 來記錄 API 合約。必要的欄位被認為是有害的,因此它們已完全從 proto3 中移除。將所有欄位設為選擇性或重複。您永遠不知道訊息類型會持續多久,以及是否有人在四年後被迫以空字串或零填寫您的必要欄位,當它在邏輯上不再需要時,proto 仍然這麼說。

對於 proto3,沒有 required 欄位,因此此建議不適用。

不要 建立具有許多欄位的訊息

不要建立具有「許多」(想想:數百個)欄位的訊息。在 C++ 中,每個欄位都會在大約增加 65 位元到記憶體物件大小,無論是否已填入(指標需要 8 個位元組,如果欄位宣告為選擇性,則在位元欄位中再增加一位元來追蹤是否設定了該欄位)。當您的 proto 成長太大時,產生的程式碼甚至可能無法編譯(例如,在 Java 中,方法的大小有硬性限制)。

在列舉中包含未指定的數值

列舉應該包含預設的 FOO_UNSPECIFIED 值,作為宣告中的第一個值。當新值新增到 proto2 列舉時,舊用戶端會將欄位視為未設定,而 getter 會傳回預設值,如果不存在預設值,則會傳回第一個宣告的值。為了與 proto 列舉保持一致的行為,第一個宣告的列舉值應該是預設的 FOO_UNSPECIFIED 值,並且應該使用標籤 0。將此預設值宣告為具有語義意義的值可能很誘人,但作為一般規則,請不要這麼做,以協助您的協定隨著時間的推移新增新的列舉值而發展。容器訊息下宣告的所有列舉值都在相同的 C++ 命名空間中,因此請在未指定的值前面加上列舉的名稱,以避免編譯錯誤。如果您永遠不需要跨語言常數,則 int32 會保留未知的值並產生較少的程式碼。請注意,proto 列舉需要第一個值為零,並且可以往返(反序列化、序列化)未知的列舉值。

不要 為列舉值使用 C/C++ 巨集常數

使用 C++ 語言已定義的文字 - 特別是在其標頭(例如 math.h)中,如果其中一個標頭的 #include 陳述式出現在 .proto.h 的陳述式之前,可能會導致編譯錯誤。避免使用巨集常數(例如「NULL」、「NAN」和「DOMAIN」)作為列舉值。

使用知名類型和通用類型

強烈建議使用下列常見的共用類型。例如,當已經存在完全合適的通用類型時,請不要在您的程式碼中使用 int32 timestamp_seconds_since_epochint64 timeout_millis

  • duration 是有正負號的固定長度時間跨度(例如,42 秒)。
  • 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 美元)。
  • latlng 是緯度/經度對(例如,37.386051 緯度和 -122.083855 經度)。
  • color 是 RGBA 色彩空間中的色彩。

在個別檔案中定義訊息類型

在定義 proto 結構描述時,您應該為每個檔案擁有單一的訊息、列舉、擴充、服務或循環相依性群組。這使得重構更容易。當檔案分開時,移動檔案比從具有其他訊息的檔案中提取訊息容易得多。遵循此實務也有助於保持 proto 結構描述檔案較小,從而提高可維護性。

如果它們會在您的專案之外被廣泛使用,請考慮將它們放在沒有相依性的個別檔案中。這樣,任何人都可以輕鬆地使用這些類型,而不會在您的其他 proto 檔案中引入遞移相依性。

如需有關此主題的詳細資訊,請參閱1-1-1 規則

不要 變更欄位的預設值

幾乎永遠不要更改 proto 欄位的預設值。這會導致客戶端和伺服器之間版本不一致。當客戶端和伺服器的建置版本橫跨 proto 變更時,客戶端讀取未設定的值會看到與伺服器讀取相同未設定的值不同的結果。Proto3 移除了設定預設值的功能。

不要 從重複類型變更為純量類型

雖然這不會導致崩潰,但會遺失資料。對於 JSON,重複性的不匹配會遺失整個訊息。對於數值型的 proto3 欄位和 proto2 的 packed 欄位,從重複到純量的變更會遺失該欄位中的所有資料。對於非數值型的 proto3 欄位和未註解的 proto2 欄位,從重複到純量的變更會導致最後反序列化的值「勝出」。

從純量變更為重複在 proto2 和帶有 [packed=false] 的 proto3 中是可以接受的,因為對於二進制序列化,純量值會變成單元素的列表。

遵循產生的程式碼的樣式指南

Proto 產生的程式碼會在一般程式碼中被引用。請確保 .proto 檔案中的選項不會導致產生違反程式碼風格指南的程式碼。例如:

不要 使用文字格式訊息進行交換

基於文字的序列化格式(如文字格式和 JSON)將欄位和列舉值表示為字串。因此,當欄位或列舉值被重新命名,或新增新的欄位、列舉值或擴展時,使用舊程式碼反序列化這些格式的 protocol buffers 會失敗。盡可能使用二進制序列化進行資料交換,僅將文字格式用於人工編輯和偵錯。

如果您在 API 中或儲存資料時使用轉換為 JSON 的 protos,您可能根本無法安全地重新命名欄位或列舉。

永遠不要 依賴跨組建的序列化穩定性

Proto 序列化的穩定性無法在不同二進制檔或同一二進制檔的不同建置版本之間保證。例如,在建立快取鍵時,不要依賴它。

不要 在與其他程式碼相同的 Java 套件中產生 Java Proto

將 Java proto 原始碼產生到與手寫 Java 原始碼不同的套件中。packagejava_packagejava_alt_api_package 選項控制產生的 Java 原始碼的輸出位置。請確保手寫的 Java 原始碼也不要放在同一個套件中。常見的做法是將您的 protos 產生到專案中的 proto 子套件中,該子套件包含這些 protos(也就是說,不包含手寫的原始碼)。

避免為欄位名稱使用語言關鍵字

如果訊息、欄位、列舉或列舉值的名稱是讀取/寫入該欄位的語言中的關鍵字,那麼 protobuf 可能會更改欄位名稱,並且可能使用與一般欄位不同的方式來存取它們。例如,請參閱這個關於 Python 的警告

您也應該避免在檔案路徑中使用關鍵字,因為這也可能導致問題。

附錄

API 最佳實務

本文檔僅列出極有可能導致損壞的變更。有關如何優雅地建立可成長的 proto API 的更高層次指導,請參閱API 最佳實務