protobuf生成Go代碼插件gogo/protobuf
從 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
代碼中的只是一些比較普通的字段定義,還有一些復雜的一些字段定義,如Oneof、Map、Reserved等可以參考官方文檔。
生成 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
gogofast、gogofaster、gogoslick: 更快的速度、會生成更多的代碼。
$ 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」前綴
gogofast類似gofast,但是會引入 gogoprotobuf 庫。gogofaster類似gogofast,但是不會產生XXX_unrecognized類的指針字段,可以減少垃圾回收時間。gogoslick類似gogofaster,但是會增加一些額外的string、gostring和equal method等。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")
}
}