原文在這裡。 本教程為 Go 程式員提供了使用Protocol buffer的基本介紹。 本教程使用proto3向 Go 程式員介紹如何使用 protobuf。通過創建一個簡單的示例應用程式,它向你展示瞭如何: 在.proto中定義消息格式 使用protocol buffer編譯器 使用Go pro ...
原文在這裡。
本教程為 Go 程式員提供了使用Protocol buffer的基本介紹。
本教程使用proto3
向 Go 程式員介紹如何使用 protobuf。通過創建一個簡單的示例應用程式,它向你展示瞭如何:
- 在
.proto
中定義消息格式 - 使用protocol buffer編譯器
- 使用Go protocol buffer API讀寫消息
這並不是protocol buffer在Go中使用的完整指南。更多細節,詳見Protocol Buffer Language Guide、Go API Reference、Go Generated Code Guide和Encoding Reference。
為什麼使用Protocol Buffer
我們要使用的例子是一個非常簡單的“通訊錄”應用程式,它可以從文件中讀寫聯繫人的信息。通訊錄中每個人都有一個姓名、ID、郵箱和練習電話。
你如何序列化並取回這樣結構化的數據呢?下麵有幾條建議:
- 原始記憶體中數據結構可以發送/保存為二進位。這是一種隨時間推移而變得脆弱的方法,因為接收/讀寫的代碼必須編譯成相同的記憶體佈局,endianness等。另外,文件已原始格式積累數據和在網路中到處傳輸副本,因此擴展這種格式十分困難。
- 你可以編寫已臨時的方法來講數據元素編碼到單個字元串中 --- 例如用“12:3:-23:67”來編碼4個int。這是一種簡單而靈活的方法,儘管它確實需要編寫一次性的編碼和解析代碼,並且解析會增加少量的運行時成本。這對於編碼非常簡單的數據最有效。
- 序列化為XML。這種方法非常有吸引力,因為XML(某種程度上)是人類可讀的,而且有許多語言的綁定庫。如果你希望與其他應用程式/項目共用數據,這可能是一個不錯的選擇。然而,XML是出了名的空間密集型,對它進行編碼/解碼會給應用程式帶來巨大的性能損失。而且,在XML DOM樹中導航要比在類中導航簡單欄位複雜得多。
Protocol buffers是解決這個問題的靈活、高效、自動化的解決方案。使用Protocol buffers,你編寫一個描述要存儲的數據結構的.proto
文件。然後,Protocol buffer編譯器會創建一個類,該類實現了Protocol buffer數據的自動編碼和解析,使用高效的二進位格式。生成的類為構成Protocol buffer的欄位提供了獲取器和設置器,並處理了讀取和寫入Protocol buffer的細節。重要的是,Protocol buffer格式支持隨著時間的推移擴展格式的想法,以使代碼仍然能夠讀取使用舊格式編碼的數據。
從哪能找到示例代碼呢?
我們的示例是一組用Protocol buffer編碼的命令行應用程式,用於管理地址簿數據文件。命令add_person_go
用於向數據文件添加新條目。命令list_people_go
解析數據文件並將數據列印到控制台。
你可以從這裡下載。
定義Protocol文件
通訊錄程式從定義.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";
接下來,需要定義message。消息只是一個包含一組類型化欄位的聚合。許多標準簡單數據類型都可用作欄位類型,包括bool
、int32
、float
、double
和string
。你也可以通過使用其他消息類型作為欄位類型來為消息添加更多結構。
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
在上面例子中,Person
消息包含PhoneNumber
消息,同時Person
消息包含在AddressBook
消息中。你甚至可以定義消息類型嵌套在其它消息中 --- 就像上面PhoneNumber
定義在Person
中。你也可以定義enum
類型,如果你想讓你的欄位只是用預定義列表中的一個值 --- 這裡你想聲明的電話類型可以是MOBILE
、HOME
或WORK
其中之一。
“= 1”,“= 2”標記每個欄位在二進位編碼中的唯一的“tag”。序號1-15編碼的位元組數比較高的數字少一位,因此,作為一種優化,你可以決定對常用或重覆的元素使用這些標記,而對不常用的可選元素使用標記16或更高。重覆欄位中的每個元素都需要重新編碼標記號,因此重覆欄位是此優化的特別好的候選項。
如果未設置欄位值,則會使用預設值:對於數字類型,使用零;對於字元串,使用空字元串;對於布爾值,使用false。對於嵌套的消息,預設值始終是消息的“預設實例”或“原型”,該實例沒有任何欄位設置。調用訪問器以獲取未明確設置的欄位的值始終返回該欄位的預設值。
如果欄位是repeated
的,那麼該欄位可以重覆任意次數(包括零次)。重覆值的順序將由protocol buffer處理。可以將重覆欄位視為動態大小的數組。
你可以在Protocol Buffer語言指南中找到撰寫.proto
文件的完整指南,包括所有可能的欄位類型。但不要尋找類繼承類似的功能 - 因為protocol buffer不支持這一點。
編譯Protocol Buffers
現在你已經有.proto
文件了,接下來你需要生成讀寫AddressBook
(包括Person
和PhoneNumber
)消息的類。現在,你需要運行protocol buffer編譯器protoc
:
- 如果你還沒安裝編譯器,可從這裡下載並根據README編譯安裝。
- 使用如下命令按照Go protocol buffers插件:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
protoc-gen-go
編譯器插件將安裝在$GOBIN
中,預設為$GOPATH/bin
。protocol buffer編譯器protoc
必須能夠在你的$PATH
中找到它。 - 現在運行編譯器,指明源目錄(應用程式源文件目錄,不指定的話預設使用當前目錄),目標路徑(你要存放生成的代碼的目錄,通常與
$SRC_DIR
一樣),.proto
文件路徑。這樣,你可以:
因為要生成Go代碼,所以使用$ protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
--go_out
選項。若要生成其它支持的語言,提供類似選項即可。
生成的github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go
文件將保存在你指定的目錄下。
Protocol Buffer API
生成的addressbook.pb.go
為你提供了下麵這些有用的類型:
- 包含
People
欄位的AddressBook
結構體 - 包含
Name
、Id
、Email
和Phones
欄位的People
- 包含
Number
和Type
欄位的Person_PhoneNumber
- 自定義枚舉類型的
Person.PhoneType
你可以在Go 生成的代碼指南中詳細瞭解生成的代碼的細節,但在大多數情況下,你可以將這些代碼視為完全普通的 Go 類型。
以下是list_people
命令的單元測試示例,演示瞭如何創建一個Person
實例:
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "[email protected]",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.Person_PHONE_TYPE_HOME},
},
}
創建Message
使用protocol buffers的目的是將數據序列化,以便在其他地方進行解析。在 Go 中,你可以使用proto
庫的Marshal函數來序列化你的protocol buffers數據。protocol buffers消息的結構體指針實現了proto.Message
介面。調用proto.Marshal
返回編碼後的protocol buffers數據。例如,我們在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)
}
讀取Message
要解析已編碼的消息,可以使用proto
庫的Unmarshal函數。調用此函數將數據解析為protocol buffers,並將結果放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定義。如果你想新的buffer可以被後向相容,並且舊的buffer可以被前向相容,--- 你確實想這樣做 --- 那你需要遵守下麵的規則。在新版的protocol buffer中:
- 你必須不能改變已有欄位的序號。
- 你可以刪除repeated欄位。
- 你可以新增repeated欄位,但必須使用新的序號(序號在protocol buffer中沒被用過,也沒被刪除)。
還有一些其它的擴展要遵守,但很少會用到它們。
遵循這些規則,舊代碼將可以輕鬆地讀取新的消息,並且會忽略任何新欄位。對於舊代碼來說,已刪除的單欄位將只是它們的預設值,而已刪除的重覆欄位將為空。新代碼也可以透明地讀取舊消息。
但請記住,舊消息中不會包含新欄位,因此你需要合理地處理預設值。使用類型特定的預設值:對於字元串,預設值是空字元串。對於布爾值,預設值是false
。對於數值類型,預設值是零。
聲明:本作品採用署名-非商業性使用-相同方式共用 4.0 國際 (CC BY-NC-SA 4.0)進行許可,使用時請註明出處。
Author: mengbin
blog: mengbin
Github: mengbin92
cnblogs: 戀水無意