ProtoBuf 生成 Go 代码去掉 JSON tag omitempty
Posted 恋喵大鲤鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ProtoBuf 生成 Go 代码去掉 JSON tag omitempty相关的知识,希望对你有一定的参考价值。
文章目录
1.背景
我们经常使用 PB(ProtoBuf)作为数据的交换协议,用于数据的序列化与反序列化。对于 PB 生成的 Go strutc,将其序列化为 JSON 时,比如对于数字类型,默认值为零,将不会出现在 JSON 串中。
为什么会这样呢?因为 PB 默认生成 的 Go struct 会带上 JSON tag omitempty,有时我们希望缺省值为零值的字段也能够出现在 JSON 串,我们需要将 struct 中的 JSON tag omitempty 去掉,那么该如何将其去掉呢?
下面将以 PB 的最新版本 proto3,来简单演示:
- PB 文件的定义
- protoc 和 protoc-gen-go 的安装
- 编译 PB 生成 Golang 代码
- 为 PB 字段自定义 JSON tag
看官莫急,且听我娓娓道来。
2.定义 proto 文件
按照官网给的 proto 示例文件 addressbook.proto ,其定义如下。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
message Person
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType
MOBILE = 0;
HOME = 1;
WORK = 2;
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;
其中syntax = "proto3"
表明我们使用版本是 proto3。
其中import "google/protobuf/timestamp.proto"
表明我们依赖 timestamp.proto。该文件可以在我们下载 protoc 的安装包中获取到,官方已经为我们打包好了。
其中package tutorial
指明当前 pb 文件所属的包,以防止不同项目的 pb 文件发生冲突。
其中option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb"
用来指明生成的 Go 文件所属的包的导入路径。路径最后一段包名。我们的示例将使用包名“tutorialpb”。当然我们也可以指定其他包命,在路径后添加个分号后写上我们想要的包命。比如我们以 addressbook 作为包名:
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb;addressbook";
3.安装 protoc 和 protoc-gen-go
我这里是在 Windows 10 环境下编译 proto 文件。按照官方给的指引,我们需要提前下载安装 protoc 和 protoc-gen-go。
protoc 是 proto 文件的编译器(protocol buffer compiler),用于将 proto 文件翻译成特定语言的类(结构)以及生成相应序列化与反序列化方法。安装详见 download the package。我这里直接下载 Windows 平台的 protoc-21.1-win64.zip,其内容如下:
- bin
- protoc.exe
- include
- readme.txt
需要将 protoc.exe 拷贝到 PATH 中的任意目录中,以保证在命令行执行它时能够找到它。我这里将其放到 GOROOT/bin 目录下。
protoc-gen-go 是用于生成 Go 代码的插件,供 protoc 使用。安装方式如下:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
编译器插件 protoc-gen-go.exe 将被安装在 GOBIN 中,默认为 GOPATH/bin。它必须位于 PATH 中,以便 protoc 能够找到它。
4. 编译 proto 文件
现在我们来编译上面的 addressbook.proto。
protoc 的命令格式如下:
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto
SRC_DIR 为待编译的 proto 文件所在目录,以及被 import 的 proto 文件所在目录,不指定默认为当前目录。DST_DIR 为生成的 Go 代码放置的目录。翻译成我的场景便是:
protoc -I"include;." --go_out=. addressbook.proto
这里需要注意的是,在 Windows 命令行指明多个 proto 文件目录时,只能使用一个 -I 或 --proto_path 选项,多个目录可以使用分号分隔,比如我这里指明 addressbook.proto 所在目录为当前目录,addressbook.proto 中 import google/protobuf/timestamp.proto 的所在目录为当前 include 目录。不像 Linux,多个目录可以使用多个 -I 选项分别指定。
message Person 最终生成的 Go struct 为:
type Person struct
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"` // Unique ID number for this person.
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
LastUpdated *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
如果我们将 Person 序列化为 JSON 时,一些零值字段序列化为 JSON 时会被忽略,即不会出现在生成的 JSON 串中。比如 Id 字段,未显示赋值时默认值为 0,那么生成的 JSON 串中将不会有字段 id。这个是由 struct 字段的 json tag 来控制的,其中 omitempty 表示忽略零值。
我们如何让生成的 struct 的 json tag 去掉 omitempty 呢?那么便需要借助 PB 的 Custom Options 功能。
5.自定义选项(Custom Options)
5.1 简介
Custom Options 是大多数人都不会用到需要的高级功能。我们不做绝大多数,所以我们来了解一下吧。
ProtoBuf 允许您定义和使用自己的选项。请注意,这是大多数人不需要的高级功能。因为选项是由 google/protobuf/descriptor .proto 中的消息定义的,如 message FileOptions 和 message FieldOptions,定义自己的选项只需扩展这些消息。例如:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions
optional string my_option = 51234;
message MyMessage
option (my_option) = "Hello world!";
在这里,我们通过扩展 MessageOptions 定义了一个新的消息级别选项。使用选项时,必须将选项名称括在括号中,以指示它是一个扩展。现在,以 C++ 为例,我们可以在代码中读取 my_option 选项的值,如下所示:
string value = MyMessage::descriptor()->options().GetExtension(my_option);
5.2 FieldOptions
回到我们的问题,想要控制 Golang 中生成 struct 时字段的 JSON tag,那么我们需要扩展 messafe FieldOptions 的定义。其留给用户自定义的 option 编号范围是 1000 至 max。
message FieldOptions
...
// Clients can define custom options in extensions of this message. See above.
extensions 1000 to max;
...
因为我么使用的是 Golang 官方插件 protoc-gen-go,其生成的 json tag 会尝试以小驼峰以及 omitempty,且没有支持改写 JSON tag 的 option 扩展。
5.3 gogoprotobuf
既然 Golang 官方插件不支持,那么我们可以诉诸业界常用的开源插件 gogoprotobuf,其有多个版本:
- protoc-gen-gofast
- protoc-gen-gogofast
- protoc-gen-gogofaster
- protoc-gen-gogoslick
因为 protoc-gen-gogofaster 在编解码方面更轻更快,且支持 gogoprotobuf extensions,满足我们自定义 JSON tag 的要求。
option | 作用于 | 类型 | 说明 |
---|---|---|---|
jsontag (beta) | Field | string | if set, the json tag value between the double quotes is replaced with this string fieldname |
因为 gogoprotobuf 在其 gogoproto/gogo.proto 已经对 google.protobuf.FieldOptions 扩展了 jsontag,所以我们直接import gogoproto/gogo.proto
就可以用其自定义 JSON tag 的 josontag 这个 option 了。
我们先安装一下 protoc-gen-gogofaster 插件。
go install github.com/gogo/protobuf/protoc-gen-gogofaster
protoc-gen-gogofaster 将被安装到 GOPATH/bin 目录下。我的 GOPATH 为D:\\go
。
接下来,我们需要在我们的 proto 文件 addressbook.proto 中 import gogoproto/gogo.proto,这样就可以使用扩展的 josontag 来自定义 JSON tag 了。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
message Person
string name = 1;
int32 id = 2 [(gogoproto.jsontag) = "id"]; // Unique ID number for this person.
string email = 3;
enum PhoneType
MOBILE = 0;
HOME = 1;
WORK = 2;
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;
我们使用插件 protoc-gen-gogofaster 重新编译一下 addressbook.proto。
protoc -I"include;." --gogofaster_out=. --plugin="protoc-gen-gogofaste=D:\\go\\bin\\protoc-gen-gogofaster.exe" addressbook.proto
成功后,我们再次看下 messafe Person 生成的 Go struct 为:
type Person struct
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Id int32 `protobuf:"varint,2,opt,name=id,proto3" json:"id"`
Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"`
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
LastUpdated *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"`
可以看到,字段 Id 的 JSON tag 已经没有了 omitempty。至此,我们大功告成。
6.小结
本文简单介绍了 proto 文件如何定义,在 Go 中如何编译生成 Go 代码。并且通过自定义 option 的方式,利用第三方插件 protoc-gen-gogofaster 完成对字段 JSON tag 的自定义,来去掉 JSON tag 中的 omitempty。
参考文献
Language Guide (proto3)
Protocol Buffer Basics: Go
在go的protobuf中进行自定义json tag标记及使用
以上是关于ProtoBuf 生成 Go 代码去掉 JSON tag omitempty的主要内容,如果未能解决你的问题,请参考以下文章
在Go中对gRPC+ProtoBuf与Http+Json进行基准测试