Protocol Buffer 基礎:Go

Go 程式設計師使用 protocol buffers 的基本介紹。

本教程旨在為 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";

接下來,是您的訊息定義。訊息只是一個包含一組型別化欄位的聚合體。許多標準的簡單資料型別都可以作為欄位型別,包括 boolint32floatdoublestring。您還可以透過使用其他訊息型別作為欄位型別,為您的訊息新增更多結構。

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_MOBILEPHONE_TYPE_HOMEPHONE_TYPE_WORK 中的一種。

每個元素上的 " = 1"、" = 2" 標記標識了該欄位在二進位制編碼中使用的唯一“標籤”(tag)。標籤號 1-15 比較大的數字少用一個位元組來編碼,因此作為一種最佳化,您可以決定將這些標籤用於常用或重複的元素,而將標籤 16 及以上的數字留給不常用的可選元素。重複欄位中的每個元素都需要重新編碼標籤號,因此重複欄位特別適合進行這種最佳化。

如果某個欄位未設定值,則會使用預設值:數字型別為零,字串為空字串,布林值為 false。對於嵌入的訊息,預設值始終是該訊息的“預設例項”或“原型”,其所有欄位都未設定。呼叫訪問器獲取未顯式設定的欄位值時,總是返回該欄位的預設值。

如果一個欄位是 repeated,該欄位可以重複任意次數(包括零次)。重複值的順序將在協議緩衝區中保留。可以把重複欄位看作是動態大小的陣列。

您可以在Protocol Buffer 語言指南中找到編寫 .proto 檔案的完整指南——包括所有可能的欄位型別。但不要去尋找類似類繼承的功能——Protocol Buffer 不支援這個。

編譯你的 Protocol Buffers

現在您有了一個 .proto 檔案,接下來需要做的就是生成讀寫 AddressBook(以及 PersonPhoneNumber)訊息所需的類。為此,您需要在您的 .proto 檔案上執行 Protocol Buffer 編譯器 protoc

  1. 如果你還沒有安裝編譯器,請下載軟體包並按照 README 中的說明進行操作。

  2. 執行以下命令安裝 Go protocol buffers 外掛

    go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
    

    編譯器外掛 protoc-gen-go 將安裝在 $GOBIN 中,預設是 $GOPATH/bin。它必須在您的 $PATH 中,以便 protocol 編譯器 protoc 能夠找到它。

  3. 現在執行編譯器,指定源目錄(您應用程式原始碼所在的位置——如果不提供值,則使用當前目錄)、目標目錄(您希望生成程式碼存放的位置;通常與 $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 結構。
  • 一個帶有 NameIdEmailPhones 欄位的 Person 結構。
  • 一個帶有 NumberType 欄位的 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。對於數字型別,預設值是零。