Protocol Buffer 基礎:Go
本教程旨在為 Go 程式設計師提供一個使用 protocol buffers 的基本介紹,其中使用 proto3 版本的 protocol buffers 語言。透過建立一個簡單的示例應用程式,本教程將向您展示如何
- 在
.proto檔案中定義訊息格式。 - 使用 Protocol Buffer 編譯器。
- 使用 Go protocol buffer API 編寫和讀取訊息。
這不是在 Go 中使用 protocol buffers 的全面指南。有關更詳細的參考資訊,請參閱 Protocol Buffer 語言指南、Go API 參考、Go 生成程式碼指南和 編碼參考。
問題領域
我們將要使用的示例是一個非常簡單的“地址簿”應用程式,它可以從檔案中讀取和寫入人們的聯絡方式。地址簿中的每個人都有姓名、ID、電子郵件地址和聯絡電話號碼。
你如何序列化和檢索這樣的結構化資料?有幾種方法可以解決這個問題:
- 使用 gobs 序列化 Go 資料結構。這在 Go 特定的環境中是一個很好的解決方案,但如果您需要與其他平臺編寫的應用程式共享資料,則效果不佳。
- 您可以發明一種特殊的方式將資料項編碼為單個字串——例如將 4 個整數編碼為“12:3:-23:67”。這是一種簡單而靈活的方法,但它需要編寫一次性的編碼和解析程式碼,並且解析會帶來一些執行時成本。這種方法最適合編碼非常簡單的資料。
- 將資料序列化為 XML。這種方法可能非常有吸引力,因為 XML(某種程度上)是人類可讀的,而且有許多語言的繫結庫。如果您想與其他應用/專案共享資料,這可能是一個不錯的選擇。然而,XML 是出了名的佔用空間,並且編碼/解碼它可能會給應用帶來巨大的效能損失。此外,遍歷 XML DOM 樹比通常情況下遍歷類中的簡單欄位要複雜得多。
Protocol Buffer 正是為解決這一問題而設計的靈活、高效、自動化的解決方案。使用 Protocol Buffer,您需要編寫一個 .proto 檔案來描述您希望儲存的資料結構。Protocol Buffer 編譯器會根據該檔案建立一個類,該類使用高效的二進位制格式實現了對 Protocol Buffer 資料的自動編碼和解析。生成的類為構成 Protocol Buffer 的欄位提供了 getter 和 setter,並負責處理將 Protocol Buffer 作為一個單元進行讀寫的細節。重要的是,Protocol Buffer 格式支援隨著時間的推移擴充套件格式,使得程式碼仍然可以讀取用舊格式編碼的資料。
在哪裡找到示例程式碼
我們的示例是一組用於管理地址簿資料檔案的命令列應用程式,使用 protocol buffers 進行編碼。命令 add_person_go 向資料檔案新增一個新條目。命令 list_people_go 解析資料檔案並將資料列印到控制檯。
您可以在 GitHub 倉庫的 examples 目錄中找到完整的示例。
定義你的協議格式
要建立您的地址簿應用,您需要從一個 .proto 檔案開始。.proto 檔案中的定義很簡單:為您想要序列化的每個資料結構新增一個訊息(message),然後為訊息中的每個欄位指定名稱和型別。在我們的示例中,定義訊息的 .proto 檔案是 addressbook.proto。
.proto 檔案以包宣告開頭,這有助於防止不同專案之間的命名衝突。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
go_package 選項定義了將包含此檔案的所有生成程式碼的包的匯入路徑。Go 包名將是匯入路徑的最後一個路徑元件。例如,我們的示例將使用包名“tutorialpb”。
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
接下來,是您的訊息定義。訊息只是一個包含一組型別化欄位的聚合體。許多標準的簡單資料型別都可以作為欄位型別,包括 bool、int32、float、double 和 string。您還可以透過使用其他訊息型別作為欄位型別,為您的訊息新增更多結構。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
在上面的示例中,Person 訊息包含 PhoneNumber 訊息,而 AddressBook 訊息包含 Person 訊息。您甚至可以在其他訊息內部定義訊息型別——如您所見,PhoneNumber 型別是在 Person 內部定義的。如果您希望某個欄位的值是預定義列表中的一個,您還可以定義 enum 型別——在這裡,您想指定一個電話號碼可以是 PHONE_TYPE_MOBILE、PHONE_TYPE_HOME 或 PHONE_TYPE_WORK 中的一種。
每個元素上的 " = 1"、" = 2" 標記標識了該欄位在二進位制編碼中使用的唯一“標籤”(tag)。標籤號 1-15 比較大的數字少用一個位元組來編碼,因此作為一種最佳化,您可以決定將這些標籤用於常用或重複的元素,而將標籤 16 及以上的數字留給不常用的可選元素。重複欄位中的每個元素都需要重新編碼標籤號,因此重複欄位特別適合進行這種最佳化。
如果某個欄位未設定值,則會使用預設值:數字型別為零,字串為空字串,布林值為 false。對於嵌入的訊息,預設值始終是該訊息的“預設例項”或“原型”,其所有欄位都未設定。呼叫訪問器獲取未顯式設定的欄位值時,總是返回該欄位的預設值。
如果一個欄位是 repeated,該欄位可以重複任意次數(包括零次)。重複值的順序將在協議緩衝區中保留。可以把重複欄位看作是動態大小的陣列。
您可以在Protocol Buffer 語言指南中找到編寫 .proto 檔案的完整指南——包括所有可能的欄位型別。但不要去尋找類似類繼承的功能——Protocol Buffer 不支援這個。
編譯你的 Protocol Buffers
現在您有了一個 .proto 檔案,接下來需要做的就是生成讀寫 AddressBook(以及 Person 和 PhoneNumber)訊息所需的類。為此,您需要在您的 .proto 檔案上執行 Protocol Buffer 編譯器 protoc:
如果你還沒有安裝編譯器,請下載軟體包並按照 README 中的說明進行操作。
執行以下命令安裝 Go protocol buffers 外掛
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest編譯器外掛
protoc-gen-go將安裝在$GOBIN中,預設是$GOPATH/bin。它必須在您的$PATH中,以便 protocol 編譯器protoc能夠找到它。現在執行編譯器,指定源目錄(您應用程式原始碼所在的位置——如果不提供值,則使用當前目錄)、目標目錄(您希望生成程式碼存放的位置;通常與
$SRC_DIR相同),以及您的.proto檔案的路徑。在這種情況下,您將呼叫:protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto因為您需要 Go 程式碼,所以您使用
--go_out選項——其他受支援的語言也提供了類似的選項。
這會在您指定的目錄中生成 github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go。
Protocol Buffer API
生成 addressbook.pb.go 會為您提供以下有用的型別
- 一個帶有
People欄位的AddressBook結構。 - 一個帶有
Name、Id、Email和Phones欄位的Person結構。 - 一個帶有
Number和Type欄位的Person_PhoneNumber結構。 - 型別
Person_PhoneType以及Person.PhoneType列舉中每個值定義的值。
您可以在 Go 生成程式碼指南 中閱讀有關具體生成內容的更多詳細資訊,但大多數情況下,您可以將這些視為完全普通的 Go 型別。
這是來自 list_people 命令的單元測試 的一個示例,展示瞭如何建立 Person 例項
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
寫入訊息
使用 protocol buffers 的全部目的是序列化您的資料,以便可以在其他地方進行解析。在 Go 中,您使用 proto 庫的 Marshal 函式來序列化您的 protocol buffer 資料。指向 protocol buffer 訊息 struct 的指標實現了 proto.Message 介面。呼叫 proto.Marshal 返回以其線路格式編碼的 protocol buffer。例如,我們在 add_person 命令 中使用此函式
book := &pb.AddressBook{}
// ...
// Write the new address book back to disk.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
讀取訊息
要解析編碼訊息,您可以使用 proto 庫的 Unmarshal 函式。呼叫此函式會將 in 中的資料解析為 protocol buffer 並將結果放入 book 中。因此,要在 list_people 命令 中解析檔案,我們使用
// Read the existing address book.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
擴充套件 Protocol Buffer
在您釋出使用 Protocol Buffer 的程式碼後,遲早會想要“改進”Protocol Buffer 的定義。如果您希望新的緩衝區向後相容,並且舊的緩衝區向前相容——您幾乎肯定希望如此——那麼您需要遵守一些規則。在新版本的 Protocol Buffer 中:
- 您*絕不能*更改任何現有欄位的標籤號。
- 您*可以*刪除欄位。
- 您*可以*新增新欄位,但必須使用新的標籤號(即,在此協議緩衝區中從未使用過的標籤號,即使是被刪除的欄位用過的也不行)。
(這些規則有一些例外,但很少使用。)
如果您遵守這些規則,舊程式碼將能夠愉快地讀取新訊息,並簡單地忽略任何新欄位。對於舊程式碼來說,被刪除的單數(singular)欄位將只有其預設值,而被刪除的重複(repeated)欄位將為空。新程式碼也將透明地讀取舊訊息。
但是,請記住,新欄位在舊訊息中是不存在的,因此您需要對預設值進行合理的處理。系統會使用特定型別的預設值:對於字串,預設值是空字串。對於布林值,預設值是 false。對於數字型別,預設值是零。