go 结构体包含接口成员进行反序列化
工作中遇到一个问题,结构体包含接口类型的成员时,在反序列化的时候会报错。
// Task 接口类型
type Task interface {
Exec() error
}
// TaskA Task接口的实现类A
type TaskA struct {
A string `json:"a"`
}
func (ta *TaskA) Exec() error {
fmt.Println("TaskA Exec")
return nil
}
// TaskB Task接口的实现类B
type TaskB struct {
B string `json:"b"`
}
func (tb *TaskB) Exec() error {
fmt.Println("TaskB Exec")
return nil
}
// MixedStruct 反序列化目标结构体
type MixedStruct struct {
Id uint64 `json:"id"`
Task Task `json:"task"`
}
场景
这个结构体在反序列化的过程中会报错。
需要给接口赋值具体的结构体才能正常反序列化。
func TestUnmarshal(t *testing.T) {
t.Run("带 Interface 的结构体反序列化", func(t *testing.T) {
str := "{\"id\":1,\"task\":{\"a\":\"a\"}}"
var mixed MixedStruct
err := jsoniter.UnmarshalFromString(str, &mixed)
assert.NotNil(t, err)
t.Log(err) // json_jsoniter.MixedStruct.Task: decode non empty interface: can not unmarshal into nil, error found in #10 byte of ...|:1,"task":{"a":"a"}}|..., bigger context ...|{"id":1,"task":{"a":"a"}}|...
mixed.Task = &TaskA{}
err = jsoniter.UnmarshalFromString(str, &mixed)
assert.Nil(t, err)
})
}
解决方案
方案一
直接干掉问题,不存接口,存JSON字符串。
优点:简单粗暴
缺点:必须在业务中解析这个字段,加大了复杂度,增加了重复工作。
方案二
在业务中给接口成员赋值。
如:
mixed.Task = &TaskA{}
_ = jsoniter.UnmarshalFromString(str, &mixed)
优点:简单粗暴
缺点:赋值成为前置条件,不同于正常的反序列化操作,在实际工程中容易遗忘。且在协同开发时必须和所有开发成员同步。
方案三
增加一个 taskType
类型,来表示实现哪个结构体
实现 UnmarshalJSON
函数
const (
TaskTypeA = 1
TaskTypeB = 2
)
type MixedStruct struct {
Id uint64 `json:"id"`
TaskType int `json:"taskType"`
Task Task `json:"task"`
}
func (a *MixedStruct) UnmarshalJSON(b []byte) error {
if a.Task == nil {
// 给接口赋值具体实现类
taskType := jsoniter.Get(b, "taskType")
switch taskType.ToInt() {
case TaskTypeA:
a.Task = &TaskA{}
case TaskTypeB:
a.Task = &TaskB{}
default:
return errors.New("illegal task type")
}
}
// 结构需要跟MixedStruct 保持一致
tmp := struct {
Id uint64 `json:"id"`
TaskType int `json:"taskType"`
Task Task `json:"task"`
}{}
tmp.Task = a.Task
if err := jsoniter.Unmarshal(b, &tmp); err != nil {
return err
}
a.Id = tmp.Id
a.Task = tmp.Task
a.TaskType = tmp.TaskType
return nil
}
优点:完美解决问题,反序列化的时候不需要额外操作
缺点:有维护成本,需要维护taskType
的枚举值,且在给MixedStruct
增加字段的时候必须对应维护UnmarshalJSON
的实现。
主要是想介绍这个方案三,对于它的缺点:
taskType
这个值基本上不论是否用接口成员,都需要维护,没有增加实际成本。- 可以把
MixedStruct
和UnmarshalJSON
放在一个文件里,并在接口成员的后面加上对应注释,起到备忘和提示的作用。
还有一点需要注意,在赋值 Task
的时候,不要用使用类似于 a.Task = b.Task
,因为 Task
是地址类型,使用的是同一片内存,修改 a.Task
的时候,b.Task
也会受影响,建议给Task
增加一个 Copy()
方法,实现深拷贝。
使用的时候碰到个问题,也记录一下
使用该结构体被当做内嵌结构体进行使用的时候,对外部结构体进行反序列化,会直接继承 UnmarshalJSON
。
type MixedStructParent struct {
MixedStruct
Extra string `json:"extra"`
}
func TestUnmarshal(t *testing.T) {
t.Run("作为内嵌结构体", func(t *testing.T) {
str := "{\"id\": 1,\"task\": {\"a\": \"a\"},\"taskType\": 1,\"extra\": \"extra msg\"}"
var mixed MixedStructParent
err := jsoniter.UnmarshalFromString(str, &mixed)
assert.Nil(t, err)
t.Log(iutil.ToJson(mixed)) // {"id":1,"taskType":1,"task":{"a":"a"},"extra":""}
})
}
如上面的 extra 反序列后就没有赋值成功,因为它直接调用了 (*MixedStruct).UnmarshalJSON
目前也没有想到什么好的兼顾的方法,暂时避免使用内嵌结构体
type MixedStructParent struct {
Mixed MixedStruct `json:"mixed"`
Extra string `json:"extra"`
}