Go 常見問題解答

關於在 Go 中實現協議緩衝區的常見問題列表,並附有每個問題的答案。

版本

github.com/golang/protobufgoogle.golang.org/protobuf 有什麼區別?

github.com/golang/protobuf 模組是原始的 Go 協議緩衝區 API。

google.golang.org/protobuf 模組是此 API 的更新版本,旨在實現簡單性、易用性和安全性。更新後的 API 的主要功能是支援反射以及使用者面向的 API 與底層實現的解耦。

我們建議您在新程式碼中使用 google.golang.org/protobuf

github.com/golang/protobufv1.4.0 及更高版本包裝了新實現,並允許程式逐步採用新 API。例如,github.com/golang/protobuf/ptypes 中定義的知名型別只是新模組中定義的那些型別的別名。因此,google.golang.org/protobuf/types/known/emptypbgithub.com/golang/protobuf/ptypes/empty 可以互換使用。

什麼是 proto1, proto2, proto3 和版本(editions)?

這些是協議緩衝區 語言 的修訂版。它與 protobufs 的 Go 實現 不同。

  • 版本(Editions)是編寫 Protocol Buffers 的最新和推薦方式。新功能將作為新版本的一部分發布。欲瞭解更多資訊,請參閱協議緩衝區版本

  • proto3 是該語言的舊版本。我們鼓勵新程式碼使用版本。

  • proto2 是該語言的舊版本。儘管已被 proto3 和版本取代,proto2 仍然得到完全支援。

  • proto1 是該語言的過時版本。它從未作為開源釋出。

有幾種不同的 Message 型別。我應該使用哪種?

常見問題

go install”: 工作目錄不是模組的一部分

在 Go 1.15 及以下版本中,您已設定環境變數 GO111MODULE=on 並在模組目錄之外執行 go install 命令。請將 GO111MODULE=auto 或取消設定該環境變數。

在 Go 1.16 及以上版本中,透過指定明確的版本可以在模組之外呼叫 go installgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest

常量 -1 溢位 protoimpl.EnforceVersion

您正在使用生成的 .pb.go 檔案,該檔案需要更新版本的 "google.golang.org/protobuf" 模組。

更新到新版本,使用

go get -u google.golang.org/protobuf/proto

未定義: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4

您正在使用生成的 .pb.go 檔案,該檔案需要更新版本的 "github.com/golang/protobuf" 模組。

更新到新版本,使用

go get -u github.com/golang/protobuf/proto

什麼是協議緩衝區名稱空間衝突?

所有連結到 Go 二進位制檔案中的協議緩衝區宣告都插入到全域性登錄檔中。

每個 protobuf 宣告(例如,列舉、列舉值或訊息)都有一個絕對名稱,它是包名.proto 原始檔中宣告的相對名稱的連線(例如,my.proto.package.MyMessage.NestedMessage)。protobuf 語言假定所有宣告都是全域性唯一的。

如果連結到 Go 二進位制檔案中的兩個 protobuf 宣告具有相同的名稱,則會導致名稱空間衝突,並且登錄檔無法透過名稱正確解析該宣告。根據所使用的 Go protobuf 版本,這將在初始化時引發恐慌(panic)或靜默丟棄衝突,並可能在執行時導致潛在的錯誤。

如何修復協議緩衝區名稱空間衝突?

解決名稱空間衝突的最佳方法取決於衝突發生的原因。

名稱空間衝突的常見發生方式

  • Vendored .proto 檔案。 當單個 .proto 檔案被生成到兩個或多個 Go 包中並連結到同一個 Go 二進位制檔案時,它會在生成的 Go 包中的每個 protobuf 宣告上發生衝突。這通常發生在 .proto 檔案被 vendored 並從中生成 Go 包時,或者生成的 Go 包本身被 vendored 時。使用者應該避免 vendoring,而是依賴於該 .proto 檔案的集中式 Go 包。

    • 如果一個 .proto 檔案由外部方擁有,並且缺少 go_package 選項,那麼您應該與該 .proto 檔案的所有者協調,以指定一個集中式的 Go 包,供大多數使用者依賴。
  • 缺少或通用的 proto 包名。如果一個 .proto 檔案沒有指定包名或使用了過於通用的包名(例如,“my_service”),那麼該檔案中宣告與宇宙中其他地方的聲明發生衝突的可能性很高。我們建議每個 .proto 檔案都有一個經過精心選擇的全域性唯一的包名(例如,以公司名稱作為字首)。

google.golang.org/protobuf 模組的 v1.26.0 版本開始,當 Go 程式啟動時,如果連結了多個衝突的 protobuf 名稱,將報告硬錯誤。雖然最好是修復衝突的來源,但可以透過兩種方式立即解決致命錯誤:

  1. 在編譯時。 處理衝突的預設行為可以透過連結器初始化的變數在編譯時指定:go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"

  2. 在程式執行時。 執行特定 Go 二進位制檔案時處理衝突的行為可以透過環境變數設定:GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main

如何使用協議緩衝區版本(editions)?

要使用 protobuf 版本,您必須在 .proto 檔案中指定該版本。例如,要使用 2023 版本,請在 .proto 檔案的頂部新增以下內容:

edition = "2023";

協議緩衝區編譯器將生成與指定版本相容的 Go 程式碼。透過版本,您還可以為 .proto 檔案啟用或停用特定功能。有關更多資訊,請參閱 協議緩衝區版本

如何控制生成的 Go 程式碼的行為?

透過版本,您可以透過在 .proto 檔案中啟用或停用特定功能來控制生成的 Go 程式碼的行為。例如,要設定實現的 API 行為,您可以在 .proto 檔案中新增以下內容:

edition = "2023";

option features.(pb.go).api_level = API_OPAQUE;

api_level 設定為 API_OPAQUE 時,協議緩衝區編譯器生成的 Go 程式碼會隱藏結構欄位,使其無法再直接訪問。相反,會建立新的訪問器方法來獲取、設定或清除欄位。

有關可用功能及其描述的完整列表,請參閱版本功能

為什麼 reflect.DeepEqual 對 protobuf 訊息的行為出乎意料?

生成的協議緩衝區訊息型別包含內部狀態,即使在等效訊息之間也可能有所不同。

此外,reflect.DeepEqual 函式不瞭解協議緩衝區訊息的語義,並且可能會報告不存在的差異。例如,包含 nil 對映的對映欄位和包含零長度、非 nil 對映的對映欄位在語義上是等效的,但會被 reflect.DeepEqual 報告為不相等。

使用 proto.Equal 函式比較訊息值。

在測試中,您還可以使用 "github.com/google/go-cmp/cmp" 包和 protocmp.Transform() 選項。cmp 包可以比較任意資料結構,而 cmp.Diff 會生成人類可讀的值差異報告。

if diff := cmp.Diff(a, b, protocmp.Transform()); diff != "" {
  t.Errorf("unexpected difference:\n%v", diff)
}

海勒姆定律(Hyrum’s Law)

什麼是海勒姆定律,為什麼它出現在這個常見問題解答中?

海勒姆定律指出

當一個 API 的使用者數量足夠多時,你承諾的契約內容就不重要了:你係統的所有可觀察行為都將被某些人所依賴。

最新版 Go 協議緩衝區 API 的一個設計目標是,在可能的情況下,避擴音供我們無法保證將來保持穩定的可觀察行為。我們的理念是,在不作任何承諾的領域,蓄意的不穩定性優於製造穩定性的假象,因為這種假象可能會在某個專案長期依賴這種錯誤假設後在將來發生改變。

為什麼錯誤文字一直在變化?

依賴於精確錯誤文字的測試是脆弱的,並且在文字更改時經常中斷。為了阻止在測試中不安全地使用錯誤文字,此模組生成的錯誤文字是故意不穩定的。

如果您需要識別錯誤是否由 protobuf 模組生成,我們保證所有錯誤都將根據 errors.Is 匹配 proto.Error

為什麼 protojson 的輸出一直在變化?

我們不保證 Go 對協議緩衝區的 JSON 格式實現的長期穩定性。該規範只規定了什麼是有效的 JSON,但沒有規定 marshaler 應該如何精確地格式化給定訊息的規範格式。為了避免給人一種輸出是穩定的假象,我們故意引入微小的差異,以便逐位元組比較很可能失敗。

為了獲得一定程度的輸出穩定性,我們建議透過 JSON 格式化程式執行輸出。

為什麼 prototext 的輸出一直在變化?

我們不承諾 Go 文字格式實現的長期穩定性。協議緩衝區文字格式沒有規範規範,我們希望保留將來改進 prototext 包輸出的能力。由於我們不承諾包輸出的穩定性,我們故意引入了不穩定性,以阻止使用者依賴它。

為了獲得一定程度的穩定性,我們建議透過 txtpbfmt 程式傳遞 prototext 的輸出。格式化程式可以直接在 Go 中使用 parser.Format 呼叫。

雜項

如何將協議緩衝區訊息用作雜湊鍵?

您需要規範化序列化,即協議緩衝區訊息的編組輸出保證隨時間保持穩定。不幸的是,目前沒有規範化序列化的規範。您需要自己編寫一個,或者找到一種避免需要它的方法。

我可以為 Go 協議緩衝區實現新增新功能嗎?

也許吧。我們總是樂於接受建議,但我們對新增新事物非常謹慎。

Go 協議緩衝區的實現力求與其他語言實現保持一致。因此,我們傾向於避免過於專注於 Go 的功能。Go 特定功能阻礙了協議緩衝區成為一種與語言無關的資料交換格式的目標。

除非您的想法特定於 Go 實現,否則您應該加入 protobuf 討論組並在那裡提出。

如果您對 Go 實現有想法,請在我們的問題跟蹤器上提交問題:https://github.com/golang/protobuf/issues

我可以為 MarshalUnmarshal 新增選項以進行自定義嗎?

僅當該選項存在於其他實現(例如,C++、Java)中時。協議緩衝區(二進位制、JSON 和文字)的編碼必須在不同實現之間保持一致,因此用一種語言編寫的程式能夠讀取另一種語言編寫的訊息。

我們不會向 Go 實現新增任何影響 Marshal 函式輸出資料或 Unmarshal 函式讀取資料的選項,除非在至少一個其他受支援的實現中存在等效選項。

我可以自定義 protoc-gen-go 生成的程式碼嗎?

一般來說,不能。協議緩衝區旨在成為一種與語言無關的資料交換格式,而特定於實現的自定義功能與該意圖背道而馳。