go中如何反序列化interface对象

工作中经常要调用别的业务团队提供的API接口。在被调方未提供SDK的情况下,需要手动执行http请求以获取相关数据。同一业务不同API的响应,基本都会遵循统一个数据格式,抽象如下:

type Base struct {
	Data interface{} `json:"data"`
}

type Info struct {
	Field1  int `json:"field1"`
	Insides []struct {
		Field2 int `json:"field2"`
	} `json:"insides"`
}

如此,就免去了对于不同接口通用外部Base部分的重复定义,只需定义各自的Info字段并将其赋值给Base.Data即可。但开发时很容易误用值赋值而非指针赋值,就会导致程序panic。

问题复现

问题的复现代码很简单,如下:

func main() {
	str := "{\"data\":{\"field1\":1,\"insides\":[{\"field2\":2}]}}"
	base := Base{
		Data: Info{},
	}
	_ = json.Unmarshal([]byte(str), &base)
	out := base.Data.(Info)
	log.Printf("%+v", out)
}

输出结果如下:

❯ go run leetcode.go
panic: interface conversion: interface {} is map[string]interface {}, not main.Outside

goroutine 1 [running]:
main.main()
        /Users/brick/personalspace/golearn/leetcode/leetcode.go:139 +0x14d
exit status 2

问题分析

panic提示信息很明显,内存中Base.Data字段为map[string]interface {},而非main.Outside。赋值后Outside每个字段都有json tag,为何go无法自动按照tag反序列化呢?json.Unmarshal源码注释中对于此问题有明确说明:

// To unmarshal JSON into an interface value,
// Unmarshal stores one of these in the interface value:
//
//	bool, for JSON booleans
//	float64, for JSON numbers
//	string, for JSON strings
//	[]interface{}, for JSON arrays
//	map[string]interface{}, for JSON objects
//	nil for JSON null

其中明确说明对于JSON objects对象,会反序列为map[string]interface{}对象。这样在使用interface断言系统自然就会panic。

问题解决

既然无法直接使用Info{}类型给Base.Data赋值,那应该如何编写代码才能满足可扩展性的需求呢?测试发现可使用&Info{}类型,打破Unmarshal对于类型的约束。目前尝试阅读源码,但并未理清相关逻辑。更新代码如下:

func main() {
	str := "{\"data\":{\"field1\":1,\"insides\":[{\"field2\":2}]}}"
	base := Base{
		Data: &Info{},
	}
	_ = json.Unmarshal([]byte(str), &base)
	out := base.Data.(*Info)
	log.Printf("%+v", out)
}

输出如下:

❯ go run leetcode.go
2022/12/11 20:51:45 *main.Info
2022/12/11 20:51:45 &{Field1:1 Insides:[{Field2:2}]}