Protocol Buffer 基礎知識:Go

Go 程式設計人員使用 Protocol Buffer 的基本介紹。

本教學課程為 Go 程式設計人員提供使用 Protocol Buffer 的基本介紹,使用 proto3 版本的 Protocol Buffer 語言。透過逐步建立簡單的範例應用程式,它將向您展示如何:

  • .proto 檔案中定義訊息格式。
  • 使用 Protocol Buffer 編譯器。
  • 使用 Go Protocol Buffer API 來寫入和讀取訊息。

這不是在 Go 中使用 Protocol Buffer 的完整指南。如需更詳細的參考資訊,請參閱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 Buffer 編碼的通訊錄資料檔案。add_person_go 命令會將新條目新增至資料檔案。list_people_go 命令會剖析資料檔案並將資料列印到主控台。

您可以在 GitHub 儲存庫的 examples 目錄中找到完整的範例。

定義您的協定格式

若要建立您的通訊錄應用程式,您需要從 .proto 檔案開始。.proto 檔案中的定義很簡單:您為要序列化的每個資料結構新增一個訊息,然後為訊息中的每個欄位指定名稱和類型。在我們的範例中,定義訊息的 .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」標記識別二進位編碼中該欄位使用的唯一「標籤」。標籤號碼 1-15 比更高的號碼需要少一個位元組來編碼,因此作為最佳化,您可以決定將這些標籤用於常用或重複的元素,將標籤 16 和更高的標籤留給較少使用的選用元素。重複欄位中的每個元素都需要重新編碼標籤號碼,因此重複欄位特別適合此最佳化。

如果未設定欄位值,則會使用預設值:數值類型為零,字串為空字串,布林值為 false。對於內嵌訊息,預設值始終是訊息的「預設執行個體」或「原型」,其中沒有設定任何欄位。呼叫存取子以取得尚未明確設定的欄位值,始終會傳回該欄位的預設值。

如果欄位是 repeated,則欄位可能會重複任意次數(包括零次)。重複值的順序將保留在 Protocol Buffer 中。將重複欄位視為動態大小陣列。

您會在Protocol Buffer 語言指南中找到編寫 .proto 檔案的完整指南 – 包括所有可能的欄位類型。不過,請不要尋找類似於類別繼承的功能 – Protocol Buffer 不執行該操作。

編譯您的 Protocol Buffers

現在您有了 .proto,您需要做的下一件事是產生讀取和寫入 AddressBook(以及 PersonPhoneNumber)訊息所需的類別。若要執行此操作,您需要在 .proto 上執行 Protocol Buffer 編譯器 protoc

  1. 如果您尚未安裝編譯器,請下載套件並依照 README 中的指示操作。

  2. 執行下列命令以安裝 Go Protocol Buffer 外掛程式:

    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 Buffer 的全部目的是序列化您的資料,以便可以在其他地方剖析它。在 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 中:

  • 絕對不能變更任何現有欄位的標籤號碼。
  • 可以刪除欄位。
  • 可以新增欄位,但您必須使用新的標籤號碼(即從未在此 Protocol Buffer 中使用過的標籤號碼,即使是已刪除的欄位也不行)。

(這些規則有一些例外情況,但很少使用。)

如果您遵循這些規則,舊程式碼將很高興地讀取新訊息,並簡單地忽略任何新欄位。對於舊程式碼,已刪除的單數欄位只會具有其預設值,而已刪除的重複欄位將為空。新程式碼也會透明地讀取舊訊息。

但是,請記住,新欄位不會出現在舊訊息中,因此您需要對預設值執行一些合理的處理。使用類型特定的預設值:對於字串,預設值為空字串。對於布林值,預設值為 false。對於數值類型,預設值為零。