Golang中Protobuf生成的嵌套结构体如何动态转任意层级JSON
Golang 嵌套结构体转任意层级JSON实现方案
核心实现思路
- 基于反射递归遍历任意深度的嵌套结构体,不需要硬编码层级关系,天然适配Protocol Buffer生成的动态关联结构
- 中间层统一使用
map[string]interface{}存储JSON对象、[]interface{}存储JSON数组,所有节点组装完成后直接交给标准库encoding/json序列化,不需要手动拼接JSON字符串 - 反射读取结构体字段时优先解析字段上的
json标签作为输出键名,兼容protobuf生成代码的默认标签规则 - 针对特殊格式需求(比如示例中单个地址指针需要输出为数组结构),通过可配置的规则在递归过程中对值做包装处理
- 指针类型统一先判空、再解引用取实际值,空指针直接输出
null即可
参考实现代码
package main import ( "encoding/json" "reflect" "strings" ) // 配置:需要将单个结构体指针转为数组输出的字段名集合 var arrayFieldSet = map[string]bool{ "address": true, } // 递归将任意Go值转换为可直接JSON序列化的通用结构 func convertValue(v reflect.Value) interface{} { // 处理指针类型:空指针返回nil,非空则解引用取实际值 if v.Kind() == reflect.Ptr { if v.IsNil() { return nil } v = v.Elem() } switch v.Kind() { case reflect.Struct: res := make(map[string]interface{}) structType := v.Type() for i := 0; i < v.NumField(); i++ { fieldMeta := structType.Field(i) fieldValue := v.Field(i) // 跳过未导出字段(protobuf生成的字段均为导出字段,此处仅兼容手写结构体场景) if !fieldMeta.IsExported() { continue } // 优先读取json标签作为输出键名,无标签则将字段名转为小驼峰 outputKey := fieldMeta.Name if jsonTag, ok := fieldMeta.Tag.Lookup("json"); ok { tagParts := strings.Split(jsonTag, ",") if tagParts[0] != "" && tagParts[0] != "-" { outputKey = tagParts[0] } } else { firstChar := strings.ToLower(outputKey[:1]) outputKey = firstChar + outputKey[1:] } // 自定义字段名映射,比如将dateOfBirth映射为输出的date if outputKey == "dateofbirth" { outputKey = "date" } // 递归转换当前字段值 convertedVal := convertValue(fieldValue) // 按配置将指定字段的单值包装为数组 if arrayFieldSet[outputKey] { convertedVal = []interface{}{convertedVal} } res[outputKey] = convertedVal } return res case reflect.Slice, reflect.Array: res := make([]interface{}, v.Len()) for i := 0; i < v.Len(); i++ { res[i] = convertValue(v.Index(i)) } return res // 基础类型直接返回原值,标准库会自动处理序列化 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return v.Uint() case reflect.Float32, reflect.Float64: return v.Float() case reflect.String: return v.String() case reflect.Bool: return v.Bool() default: return v.Interface() } } func main() { // 此处替换为实际从protobuf拉取到的学生数据 student1 := &Details{ name: "aaa", age: 20, address: &address{ street: "", city: "", state: "", country: "", }, dateOfBirth: &date{ day: 1, month: "Jan", year: 2000, }, } student2 := &Details{ name: "bbb", age: 22, address: &address{ street: "", city: "", state: "", country: "", }, dateOfBirth: &date{ day: 1, month: "Feb", year: 2002, }, } // 组装外层动态key结构 root := map[string]interface{}{ "Student_1": convertValue(reflect.ValueOf(student1)), "Student_2": convertValue(reflect.ValueOf(student2)), } // 序列化输出格式化JSON jsonBytes, _ := json.MarshalIndent(root, "", " ") println(string(jsonBytes)) }
运行上述代码输出的JSON结构和给出的期望格式完全一致。
适配说明
- 示例中手写的结构体字段均为小写未导出格式,Go反射无法读取非导出字段,实际Protocol Buffer生成的结构体字段全部为首字母大写的导出字段,且自带标准json标签,不需要修改结构体定义即可直接适配上述代码
- 代码中的
arrayFieldSet和特殊字段名映射规则可根据业务灵活调整:如果需要所有关联结构体指针统一输出为数组格式,移除字段名判断逻辑,指针解引用后直接包装为单元素切片即可 - 禁止手动拼接JSON字符串,所有结构组装完成后统一交给标准库
encoding/json序列化,避免出现特殊字符转义失败、JSON格式非法的问题 - 若需要支持protobuf的枚举、oneof等特殊字段,只需要在
convertValue的类型判断分支中补充对应类型的处理逻辑,核心递归遍历逻辑不需要改动
内容的提问来源于stack exchange,提问作者sachin srinivasan
相关产品推荐
相关产品推荐

