19 go语言(golang) - 通过反射手动实现json序列化

一、json

在 Go 语言中,JSON 序列化和反序列化通常通过标准库 encoding/json 来实现。这个包提供了简单易用的接口来将 Go 数据结构转换为 JSON 格式字符串(序列化),以及从 JSON 字符串解析出 Go 数据结构(反序列化)。

1.1 序列化(将 Go 对象转换为 JSON)

使用 json.Marshal() 函数可以将一个 Go 对象转换为 JSON 字符串。

import (
	"encoding/json"
	"fmt"
	"testing"
)

type TestJson struct {
	UserId       int    `json:"user_id"` // 使用结构体标签可以指定字段在 JSON 中的键名
	UserNickname string // 如果没有指定标签,则默认使用字段名。
	UserAge      int    `json:"age,omitempty"` // 使用omitempty可以在序列化时忽略空值
}

func Test1(t *testing.T) {
	tJson1 := TestJson{1, "Jackson", 18}
	tJson2 := new(TestJson)
	jsonStr1, _ := json.Marshal(tJson1)
	jsonStr2, _ := json.Marshal(tJson2)

	fmt.Println(string(jsonStr1))
	fmt.Println(string(jsonStr2))
}

输出

{"user_id":1,"UserNickname":"Jackson","age":18}
{"user_id":0,"UserNickname":""}

美观打印的序列化

如果需要生成格式良好的、可读性更高的输出,可以使用 json.MarshalIndent()

func Test2(t *testing.T) {
	tJson1 := TestJson{1, "Jackson", 18}
	jsonStr1, _ := json.MarshalIndent(tJson1, "", "\t")

	fmt.Println(string(jsonStr1))
}

输出

{
	"user_id": 1,
	"UserNickname": "Jackson",
	"age": 18
}

1.2 反序列化(将 JSON 转换为 Go 对象)

使用 json.Unmarshal() 函数可以将一个 JSON 字符串解析到相应的 Go 数据结构中。

func Test3(t *testing.T) {
	jsonStr :=
		`{
			"user_id": 1,
			"UserNickname": "Jackson",
			"age": 18
		}`
	var tJson TestJson

	// 将字符串转为字节数组,再传入解析后的对象指针
	// 第二个参数必须是指向目标数据类型变量的指针,以便函数能够修改该变量。
	err := json.Unmarshal([]byte(jsonStr), &tJson)

	if err != nil {
		fmt.Println("解析错误:", err)
	} else {
		fmt.Println(tJson)
	}
}

输出

{1 Jackson 18}

1.3 注意

  • 字段导出:只有导出的字段(即首字母大写)才能被编码/解码。

  • 错误处理:始终检查返回错误,以确保数据正确处理。

  • 多余的信息:解析的结构体之外的字段会被丢弃

    func Test5(t *testing.T) {
    	jsonStr :=
    		`{
    			"user_id": 1,
    			"UserNickname": "Jackson",
    			"age": 18,
    			"addr": ["地址1","地址2"],
    			"info":{
    				"id":"2",
    				"name":"a"
    			}
    		}`
    	var tJson TestJson
    
    	err := json.Unmarshal([]byte(jsonStr), &tJson)
    
    	if err != nil {
    		fmt.Println("解析错误:", err)
    	} else {
    		fmt.Println(tJson)
    	}
    }
    

    输出

    {1 Jackson 18}
    
  • 灵活性:对于未知或动态数据,可以考虑使用 map 或 interface{} 来接收解码结果,但这会丧失一些类型安全特性。

    func Test4(t *testing.T) {
    	jsonStr :=
    		`{
    			"user_id": 1,
    			"UserNickname": "Jackson",
    			"age": 18,
    			"addr": ["地址1","地址2"],
    			"info":{
    				"id":"2",
    				"name":"a"
    			}
    		}`
    	var tJson map[string]any
    
    	err := json.Unmarshal([]byte(jsonStr), &tJson)
    
    	if err != nil {
    		fmt.Println("解析错误:", err)
    	} else {
    		fmt.Println(tJson)
    	}
    }
    

    输出

    map[UserNickname:Jackson addr:[地址1 地址2] age:18 info:map[id:2 name:a] user_id:1]
    

二、反射手动实现

encoding/json 包在内部大量使用了反射来实现其功能。

JSON 序列化中的反射

  • 在调用 json.Marshal() 时,Go 使用反射来检查传入对象的类型。
  • 通过 reflect.TypeOf()reflect.ValueOf() 获取类型信息和实际值。
  • 遍历结构体字段,读取标签(如 json:"name"),并根据字段类型生成相应的 JSON 字符串。

JSON 反序列化中的反射

  • 在调用 json.Unmarshal() 时,Go 使用目标变量指针,通过反射确定需要填充的数据结构。
  • 根据 JSON 数据中的键名,通过标签映射找到对应结构体字段,并设置其值。

2.1 json简单序列化

思路:模仿 encoding/json 库,读取结构体标签指定序列化的字段名

  1. 反射传入的结构体
  2. 获取字段
  3. 判断字段的标签
  4. 拼接字符串
func serializeSimple(data interface{}) string {
	var resultStr string = "{"

	//1、反射传入的结构体
	reflectDataValue := reflect.ValueOf(data)
	reflectDataType := reflectDataValue.Type()
	if reflectDataValue.Kind() == reflect.Ptr {
		reflectDataValue = reflectDataValue.Elem()
	}
	if reflectDataType.Kind() == reflect.Ptr {
		reflectDataType = reflectDataType.Elem()
	}

	//2、获取字段
	for i := 0; i < reflectDataType.NumField(); i++ {
		field := reflectDataType.Field(i)

		// 字段名
		var filedName = field.Name
		// 字段的值
		filedValue := reflectDataValue.Field(i).Interface()

		//3、判断字段的标签
		if value, ok := field.Tag.Lookup("json"); ok {
			// 如果有json标签,则使用其定义的命名
			filedName = strings.ReplaceAll(value, ",omitempty", "")

			// 是否忽略空值
			if strings.Contains(value, "omitempty") {
				if filedValue == "" || filedValue == 0 {
					continue
				}
			}
		}

		// 拼接json
		resultStr += fmt.Sprintf("\"%s\":\"%v\"", filedName, filedValue)
		resultStr += ","

	}

	//4、拼接字符串
	resultStr += "}"
	// 去掉结尾的,号
	return strings.ReplaceAll(resultStr, ",}", "}")
}

func Test6(t *testing.T) {
	tJson1 := TestJson{1, "Jackson", 18}
	tJson2 := new(TestJson)
	jsonStr1, _ := json.Marshal(tJson1)
	jsonStr2, _ := json.Marshal(tJson2)

	fmt.Println(string(jsonStr1))
	fmt.Println(string(jsonStr2))

	fmt.Println("=========自定义实现=========")

	fmt.Println(serializeSimple(tJson1))
	fmt.Println(serializeSimple(tJson2))

}

输出

{"user_id":1,"UserNickname":"Jackson","age":18}
{"user_id":0,"UserNickname":""}
=========自定义实现=========
{"user_id":"1","UserNickname":"Jackson","age":"18"}
{"user_id":"0","UserNickname":""}

2.2 json简单反序列化

思路

  1. 切割传入的字符串,拆分成key value的键值对
  2. 反射结构体,判断结构体标签后生成一个字段和值的map
  3. 判断字段类型,set进对应类型的值
func parseSimple(str string, dataTemp interface{}) {
	// 判断是否标准的json格式数据
	if str != "" && str[0] == '{' && str[len(str)-1] == '}' {

		// 替换掉前后的{}
		str = strings.ReplaceAll(strings.ReplaceAll(str, "{", ""), "}", "")

		// 将结构体的标签解析后,塞入map中备用 :map [字段名] 字段地址
		structMap := map[string]reflect.Value{}
		// 通过反射获取字段名
		rValue := reflect.ValueOf(dataTemp)
		if rValue.Kind() == reflect.Ptr {
			rValue = rValue.Elem()
		}
		rType := rValue.Type()
		for i := 0; i < rType.NumField(); i++ {
			name := rType.Field(i).Name
			// 如果有定义标签,则使用标签的字段名
			if lookup, ok := rType.Field(i).Tag.Lookup("json"); ok {
				name = strings.ReplaceAll(lookup, ",omitempty", "")
			}
			// 将字段名和值映射起来
			structMap[name] = rValue.Field(i)
		}

		// 按照,切割每个键值对
		splitList := strings.Split(str, ",")
		for i := range splitList {
			s := splitList[i]

			// 按照:切割出key 和 value
			keyValue := strings.Split(s, ":")
			key := keyValue[0]
			key = strings.ReplaceAll(strings.TrimSpace(key), "\"", "") // 去除前后的空格
			value := keyValue[1]
			value = strings.ReplaceAll(strings.TrimSpace(value), "\"", "")

			// 按照key将value塞回结构体
			switch structMap[key].Type().Kind() {
			// 注意判断类型,目前只写int和string,其他的类似
			case reflect.Int:
				intValue, _ := strconv.Atoi(value)
				structMap[key].SetInt(int64(intValue))
			case reflect.String:
				structMap[key].SetString(value)
			default:
				panic("暂不支持的数据类型")
			}

		}
	}

}
func Test7(t *testing.T) {
	jsonStr :=
		`{
			"user_id": 1,
			"UserNickname": "Jackson",
			"age": 18
		}`
	var tJson1 TestJson
	var tJson2 TestJson

	err := json.Unmarshal([]byte(jsonStr), &tJson1)

	if err != nil {
		fmt.Println("解析错误:", err)
	} else {
		fmt.Println(tJson1)
	}

	fmt.Println("=========自定义实现=========")
	parseSimple(jsonStr, &tJson2)
	fmt.Println(tJson2)

}

2.3 问题

  1. 只能处理结构体类型的数据,不支持嵌套复杂数据结构(如切片或映射)
  2. 在序列化的实现中,整数或布尔值会被转换为带引号的字符串形式。
  3. 复杂的结构体不支持,可能会出问题

所以手动实现 JSON 序列化和反序列化只是帮助我们更好地理解数据格式与程序语言之间的映射关系,在实际开发中,使用标准库 encoding/json 是更高效且可靠的方法,因为它已经考虑了许多复杂情况,并进行了性能优化。

2.4 总结

2.4.1 单引号和双引号定义的字符串有什么区别

在 json简单反序列化的例子中,使用了单引号和双引号定义不同类型的字符数据。具体的区别是啥呢?

  1. 单引号 (')

    • 单引号用于表示一个字符(rune)。在 Go 中,rune 是一个别名,代表 int32 类型,用于表示 Unicode 码点。
    • 一个用单引号括起来的字符常量只能包含一个字符。例如:'a', '中', '😊'
  2. 双引号 (")

    • 双引号用于定义字符串(string)。字符串是由一系列字节组成的数据类型,可以包含零个或多个字符。
    • 字符串可以包括转义序列,例如:\n, \t, \" 等。
    • 例如:“hello”, “世界”, “Go is fun\n”.

2.4.2 string如何转为int

在 json简单反序列化的例子中,使用了strconv.Atoi函数将字符串(string)转换为整数(int),除此之外还有strconv.ParseInt

  • strconv.Atoi 函数将字符串转换为int类型。如果转换成功,它返回转换后的整数和nil错误;如果失败,它返回0和错误。

  • strconv.ParseInt 函数将字符串转换为int64类型,并且允许你指定基数和位大小。如果你需要转换为int类型,你可能需要根据平台(32位或64位)来决定如何处理结果。

    func Test8(t *testing.T) {
    	str := "123"
    	// 第二个参数是基数,10表示十进制
    	// 第三个参数是位大小,0表示int64,32表示int32,使用32或64取决于你的系统架构
    	num, err := strconv.ParseInt(str, 10, 0)
    	if err != nil {
    		fmt.Println("转换错误:", err)
    	} else {
    		fmt.Println("转换结果:", num)
    		// 如果你需要int32,可以这样转换
    		num32 := int32(num)
    		fmt.Println("转换为int32结果:", num32)
    		// 如果你需要int,可以这样转换(注意:在32位系统上这将是int32,在64位系统上这将是int64)
    		numInt := int(num)
    		fmt.Println("转换为int结果:", numInt)
    	}
    }
    

    输出:

    转换结果: 123
    转换为int32结果: 123
    转换为int结果: 123
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序猿-瑞瑞

打赏不准超过你的一半工资哦~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值