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的实现。

主要是想介绍这个方案三,对于它的缺点:

  1. taskType 这个值基本上不论是否用接口成员,都需要维护,没有增加实际成本。
  2. 可以把 MixedStructUnmarshalJSON 放在一个文件里,并在接口成员的后面加上对应注释,起到备忘和提示的作用。

还有一点需要注意,在赋值 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"`
}