protobuf 生成 Go 程式碼外掛 gogo/protobuf

bazinga 發表於 2021-03-28

從 JSON 開始

談到序列化,大家最先想到的可能是 JSON 或者 XML,這兩種序列化協議都是基於文字的編碼方式進行資料傳輸。類似的還有 YAML 等。

JSON 擁有許多優點,使之成為最廣泛使用的序列化協議之一。如 JSON 協議簡單,人眼可讀,序列化後十分簡潔且解析速度快。此外,JSON 具備 JavaScript 的先天性支援,被廣泛應用於 Web Browser 的應用場景中,並且是 Ajax 的事實標準協議。

JSON 的適用場景比較多,典型應用場景包括:

  • 公司外部之間傳輸資料量相對較小,實時性要求相對低的服務
  • 基於 Web browser 的 Ajax 請求
  • 介面經常發生變化,並對可調式性要求較高的場景,例如移動 App 與服務端的通訊

然而,由於 JSON 本身的設計的一些特點,在一些場景下使用 JSON 仍然不是最優解。如:

  • 需要標準的 IDL ,增強參與各方業務約束的場景。由於 JSON 協議往往只能使用文件的方式來進行約定,這可能會給除錯帶來一些不便與不明確
  • 對效能和簡潔性有較高要求的場景。JSON 在一些語言中的序列化和反序列化需要採用反射機制,所以在效能要求特別高場景下可能不是最優解

  • 對於大資料量服務或持久化場景。JSON 進行序列化的額外空間開銷比較大,這也意味著較大的記憶體和磁碟開銷

對於以上場景, 使用一些基於 IDL ,儲存方案為二進位制儲存的序列化方案則更為合適, 如 ProtoBuf、Thrift、avro 等。

IDL: 參與通訊的各方需要對通訊的內容需要做相關的約定。為了建立一個與語言和平臺無關的約定,這個約定需要採用與具體開發語言、平臺無關的語言來進行描述。這種語言被稱為介面描述語言(IDL),採用 IDL 撰寫的協議約定稱之為 IDL 檔案。

什麼是 Protobuf

ProtoBuf 是 Protocol Buffers 的簡稱 ,是 Google 公司開源的一種語言無關、平臺無關、可擴充套件的序列化結構資料的方案,它可用於(資料)通訊協議、資料儲存等。

ProtoBuf 是上述場景中比較適用的序列化方案之一。 ProtoBuf 非常靈活,高效,我們可以通過定義 IDL(在這裡是 proto)檔案,然後使用生成的原始碼輕鬆的在各種資料流中使用各種語言進行編寫和讀取結構資料。甚至可以更新資料結構,而不破壞由舊資料結構編譯的已部署程式。

上文提到,同型別的序列化方案還有 Thrift 和 Avro。其中 Thrift 並不僅僅是序列化協議,他被嵌入到 Thrift 框架中,這導致其很難和其他傳輸層協議共同使用。Avro 由於沒有成熟的 JS 實現,不適合 Web 環境, 也 導致其使用場景也比較有限。

目前 gRPC 預設的序列化方式是 ProtoBuf。

ProtoBuf 包含序列化格式的定義、各種語言的庫以及一個 IDL 編譯器。正常情況下需要我們定義 proto 檔案,然後使用 IDL 編譯器編譯成需要的語言。

一個簡單的 proto 例子

syntax = "proto3";                // proto 版本,建議使用 proto3
option go_package = "main/proto"; // 包名宣告符

message SearchRequestParam {      // message 型別
  enum Type {                     // 列舉型別
    PC = 0;
    Mobile = 1;
  }
  string query_text = 1;          // 字串型別 | 後面的「1」為數字識別符號,在訊息定義中需要唯一
  int32 limit = 3;                // 整型
  Type type = 4;                  // 列舉型別
}

message SearchResultPage {
  repeated string result = 1;     // 「repeated」表示欄位可以重複任意多次(包括0次)
  int32 num_results = 2;
}
// test.proto

程式碼中的只是一些比較普通的欄位定義,還有一些複雜的一些欄位定義,如OneofMapReserved等可以參考官方文件。

生成 Go 程式碼

.proto 檔案中定義好需要處理的結構化資料後,可以通過 protoc 工具,將 .proto 檔案轉換為 C、C++、Golang、Java、Python 等多種語言的程式碼。我們這裡嘗試一下生成 Golang 語言程式碼。

首先需要安裝 protoc 工具

# 下載安裝包 (Mac)
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v3.15.6/protoc-3.15.6-osx-x86_64.zip
# 解壓到 /usr/local 目錄下
$ unzip protoc-3.15.6-osx-x86_64.zip -d protoc-3.15.6-osx-x86_64
$ mv protoc-3.5.0-osx-x86_64/bin/protoc /usr/local/bin/protoc
# 執行如下表示成功:
$ protoc --version
libprotoc 3.15.6

然後安裝一個官方的生成 Golang 程式碼的外掛 protoc-gen-go

$ go get -u github.com/golang/protobuf/protoc-gen-go

現在在 proto檔案所在目錄下執行以下命令以生成 go 檔案

$ protoc --go_out=. test.proto

protoc 命令還可以使用-I引數指定搜尋 import 的 proto 的資料夾。其他引數詳情可以參考官方文件。

我們可以在目錄下看到一個 test.pb.go 檔案。其中主要結構體如下:

type SearchRequestParam struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    QueryText string                  `protobuf:"bytes,1,opt,name=query_text..."`    
    Limit     int32                   `protobuf:"varint,3,opt,name=limit,proto3"...."`                           
    Type      SearchRequestParam_Type `protobuf:"varint,4,opt,name=type,proto3..."`
}
type SearchResultPage struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Result     []string `protobuf:"bytes,1,rep,name=result,proto3...."`
    NumResults int32    `protobuf:"varint,2,opt,name=num_results,json=numResults,proto3..."`

接下來,就可以在專案程式碼中直接使用了。

gogo/protobuf 是什麼

在上文中,我們安裝了一個「生成 Golang 程式碼的外掛 protoc-gen-go」,這個外掛其實是 golang 官方提供的 一個 Protobuf api 實現。而我們的主角gogo/protobuf是基於 golang/protobuf 的一個增強版實現。

gogo 庫基於官方庫開發,增加了很多的功能,包括:

  • 快速的序列化和反序列化
  • 更規範的 Go 資料結構
  • goprotobuf 相容
  • 可選擇的產生一些輔助方法,減少使用中的程式碼輸入
  • 可以選擇產生測試程式碼和 benchmark 程式碼
  • 其它序列化格式

目前很多知名的專案都在使用該庫,如 etcd、k8s、tidb、docker swarmkit 等。

gogo/protobuf 如何使用

https://github.com/gogo/protobuf 根目錄下我們可以看到有很多資料夾,其中「protoc-gen」為字首的為生成程式碼的外掛,其他「proto」、「protobuf」、「gogoproto」等為庫檔案。

gogo 庫目前有三種生成程式碼的方式

  • gofast: 速度優先,但此方式不支援其它 gogoprotobuf 的擴充套件選項。
$ go get github.com/gogo/protobuf/protoc-gen-gofast
$ protoc --gofast_out=. myproto.proto
  • gogofastgogofastergogoslick: 更快的速度、會生成更多的程式碼。

    • gogofast類似gofast,但是會引入 gogoprotobuf 庫。
    • gogofaster類似gogofast,但是不會產生XXX_unrecognized類的指標欄位,可以減少垃圾回收時間。
    • gogoslick類似gogofaster,但是會增加一些額外的stringgostringequal method等。
$ go get github.com/gogo/protobuf/proto
$ go get github.com/gogo/protobuf/{binary} //protoc-gen-gogofast、protoc-gen-gogofaster 、protoc-gen-gogoslick 
$ go get github.com/gogo/protobuf/gogoproto
$ protoc -I=. -I=$GOPATH/src -I=$GOPATH/src/github.com/gogo/protobuf/protobuf --{binary}_out=. myproto.proto // 這裡的{binary}不包含「protoc-gen」字首
  • protoc-gen-gogo: 最快的速度,最多的可定製化

    • 可以通過擴充套件選項高度定製序列化。
$ go get github.com/gogo/protobuf/proto
$ go get github.com/gogo/protobuf/jsonpb
$ go get github.com/gogo/protobuf/protoc-gen-gogo
$ go get github.com/gogo/protobuf/gogoproto

gogo/protobuf 提供了非常多的擴充套件選項,以便在產生程式碼的時候進行更多的控制。上文提到的擴充套件選項這裡有一個全面的介紹:extensions,擴充套件選項裡主要包含一些生成快速序列化反序列化程式碼的可選項、生成更規範的 Golang 資料結構的可選項、goprotobuf 相容的可選項,一些產生輔助方法的可選項、產生測試程式碼和 benchmark 的可選項,還可以增加 jsontag 等。

有同學對以上多個生成方式的序列化效能做了一些壓測,在一般需求下,效能差距並不是很大,protoc-gen-gofast方式基本可以滿足大多數場景。

最後,生成的 go 語言程式碼在專案中使用就非常簡單了,一般只需要使用proto.Marshal,proto.Unmarshal 方法就可以了,下面是一個例子:

package main

import (
    "fmt"
    "log"

    zaproto "git.xxxxx.com/data/za-proto/proto"
    "github.com/gogo/protobuf/proto"
)

func main() {
    req := &zaproto.SearchRequestParam{
        QueryText: "xxxxxx",
        Limit:     10,
        Type:      zaproto.SearchRequestParam_PC,
    }
    data, err := proto.Marshal(req)
    if err != nil {
        log.Fatal("Marshal err : err")
    }
    // send data
    fmt.Println(string(data))

    var respData []byte
    var result = zaproto.SearchResultPage{}
    if err = proto.Unmarshal(respData, &result); err == nil {
        fmt.Println(result)
    } else {
        log.Fatal("Unmarshal err : err")

    }
}

參考

alecthomas/go_serialization_benchmarks: Benchmarks of Go serialization methods (github.com)

So you want to use GoGo Protobuf (jbrandhorst.com)

Schema evolution in Avro, Protocol Buffers and Thrift — Martin Kleppmann’s blog

Language Guide | Protocol Buffers | Google Developers

序列化和反序列化 - 美團技術團隊 (meituan.com)

Protobuf 有沒有比 JSON 快 5 倍?-InfoQ

幾種 Go 序列化庫的效能比較 | 鳥窩 (colobu.com)

思考 gRPC :為什麼是 protobuf | 橫雲斷嶺的專欄 (hengyunabc.github.io)

幾種 Go 序列化庫的效能比較 | 鳥窩 (colobu.com)

更多原創文章乾貨分享,請關注公眾號
  • protobuf 生成 Go 程式碼外掛 gogo/protobuf
  • 加微信實戰群請加微信(註明:實戰群):gocnio