一、问题背景
再Go语言开发中,Protobuf(Protocol Buffers)是一种广泛使用的数据序列化和反序列化工具,它具有高效、跨语言等优点。然而,当我们使用Protebuf生成的结构体进行JSON序列化是,会遇到一个令人困扰的问题:Protobuf生成的代码里,结构体的JSON通常标签带有omitempty选项。这就导致再JSON序列化的过程中,若字段的值为零值(像布尔类型的false、空字符串、nil等),这些字段会被忽视,不会出现再最终的JSON输出里面。
通常情况下,我们是无法直接修改的Protobuf结构体的omitempty字段。这是因为是经过封装和预编译的,修改其源码可能会破坏整个库的稳定性和兼容性,并且再后续更新库时,我们的修改也会被覆盖。例如我要调用的业务通过Protobuf定义了数据结构,我们作为调用方无法随意修改器生成的结构体标签。假设定义了如下一个简单的Protobuf结构拖
syntax = "proto3";
package example;
message TaskInfo {
bool scheduled = 1;
string id = 2;
}
Protobuf 工具生成的 Go 结构体可能类似如下
type TaskInfo struct {
Scheduled bool `protobuf:"varint,1,opt,name=scheduled,proto3" json:"scheduled,omitempty"`
Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"`
}
当我们想要将这个结构体序列化为JSON时,如果Scheduled为false时, 它将不会出现再最终的JSON数据中,这不符合我们想要展示完整字段的诉求。
二、解决方案探索
为了解决上面的问题,我们探索了多种可行的方案
1.手动修改Protobuf 模板
理论上,我们可以手动修改Protobuf的代码生成的模板,让生成的Go结构体不包含omitempty标签,但这种方法的实现难度大,需要深入了解Protobuf的代码生成机制,并且修改后的模板再后续更新Protobuf工具时可能出现兼容性问题,维护成本高。
2.手动复制结构体
手动把Protobuf生成的结构体复制到一个新的自定义结构体中,新的结构体的JSON标签不包含omitempt选项。这种方法实现起来简单,但当结构体字段比较多时,手动复制工作量大,且代码的可维护性差,一旦Protocol发生变化,我们还需要手动更新自定义结构体。
3.使用反射机制
通过反射遍历结构体的字段,动态的创建一个新的结构体或map,并将字段值复制过去。反射虽然能自动化处理,但会带来一定的性能开销,并且代码复杂度较高,调试和维护都有一定难度。
4.两次JSON序列化
先将源结构体序列化为JSON字符串,再把这个字符串反序列化到目标结构体。由于中间的JSON字符串不包含omitempt信息,所以在反序列化之后,目标结构体的JSON输出会包含所有字段,包含零值字段。这种方法相对简单,不需要引入额外的依赖库,性能开销也相对较小。
经过权衡,我们最终选择两次JSON序列化的方案,它在开发效率、维护成本和性能之间取得了很好的平衡。
三、具体实现
1.重新定义结构体
我们重新定义一个不包含 omitempty 标签的目标结构体RespTaskInfo,确保所有字段都会在 JSON 输出中出现。
type RespTaskInfo struct {
Scheduled bool `json:"scheduled"`
ID string `json:"id"`
}
2.实现转换函数
编写一个通用的转换函数 ConvertObjectUseJSON,用于完成两次 JSON 序列化和反序列化操作。
// 通用转换函数
func ConvertObjectUseJSON(src interface{}, dest interface{}) error {
// 第一次序列化:忽略 omitempty
data, err := json.Marshal(src)
if err != nil {
return err
}
// 第二次反序列化:填充目标结构体
return json.Unmarshal(data, dest)
}
3.使用转换函数
在实际代码中调用转换函数,实现忽略 omitempty 的 JSON 序列化。
func main() {
// 创建一个 Protobuf 生成的 TaskInfo 实例
originalTask := pb.TaskInfo{
Scheduled: false,
ID: "123",
}
// 用于存储转换后的结果
var convertedTask RespTaskInfo
// 调用转换函数
if err := ConvertObjectUseJSON(originalTask, &convertedTask); err != nil {
fmt.Printf("转换失败: %v\n", err)
return
}
// 将转换后的结构体进行 JSON 序列化
jsonData, err := json.Marshal(convertedTask)
if err != nil {
fmt.Printf("JSON 序列化失败: %v\n", err)
return
}
// 输出 JSON 结果
fmt.Println(string(jsonData))
}
运行上述代码,你会发现即使 Scheduled 字段的值为 false,它也会出现在最终的 JSON 输出中,这表明我们的解决方案达到了预期效果,成功解决了 Protobuf 结构体 JSON 序列化时忽略零值字段的问题,保证了传给前端的结构体没有忽略零值字段
四、总结
在处理Go中Protobuf结构体JSON序列化的omitempty问题时,由于无法直接修改对方Protobuf结构体标签,我们探索了多种解决方案。最终,通过重新定义结构体并使用两次JSON序列化的方法,在不破坏稳定性和兼容性的前提下,实现了完整的JSON序列化,包含所有非零字段。这种方法在开发效率、维护成本和性能之间找到合适的平衡点,希望为遇到类似问题的开发者提供参考。