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)Fieldstringif 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的主要内容,如果未能解决你的问题,请参考以下文章

解决protobuf忽略空返回值的问题

一定义 protobuf 消息并生成 Go 代码

在Go中对gRPC+ProtoBuf与Http+Json进行基准测试

在Go中使用Protobuf

比起 JSON 更方便更快速更簡短的 Protobuf 格式

详解 Go 语言 Protobuf 之 Message