You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 01:54:34