cover

Go 类型内嵌在反序列化中的妙用与陷阱

sorcererxw

在 Go 当中 strcut embeding 往往被认为是替代面向对象中的 extends 的语法糖。除了简单实现一些面向对象的建模工作,通过这种方式实现字段/方法覆盖在 JSON 反序列化当中也有一些奇技淫巧。

Effective Go - The Go Programming Language
https://go.dev/doc/effective_go#embedding

字段类型替换

可以使用类型内嵌 override 内嵌类型中字段的类型,一个典型的使用场景:

type Inner struct {
	CreateTime time.Time `json:"time"`
}

type Alias Inner

type Outer struct {
	CreateTime int64 `json:"time"`

	*Alias
}

func (i *Inner) UnmarshalJSON(b []byte) error {
	var o Outer
	o.Inner = (*Alias)(i)
	
	json.Unmarshal(b,&o)

	i.CreateTime = time.Unix(o.CreateTime,0)

	return nil
}

众所周知,Go 中的 time.Time 只能反序列化 RFC3339 格式的时间戳,那么如果需要反序列化 Unix 时间戳咋办?可以自定义一次 Inner 结构体的 UnmarshalJSON 方法,在其中使用 Outer 结构体覆盖 CreateTime 字段,这样反序列化过程中,当解析到 time 字段的时候,就会尝试将值写入到 Outer.CreateTime 字段当中,绕过了 Inner.CreateTime。

不过有个需要注意的点,Outer 当中需要使用 Alias 类型替换 Inner 类型,否则序列化过程中,json 库会检测到 Outer 实现了 UnmarshalJSON 方法(其实是 Inner.UnmarshalJSON),而直接调用 Inner.Unmarshal,不会尝试向 Outer 填充数据。

UnmarshalJSON 地狱

正如刚才提到的,如果一个类型内嵌了另外一个实现了 UnmarshalJSON 的类型,那么 json 默认会直接调用内嵌类型的 UnmarshalJSON。

如果想要反序列化过程当中,保持从外到内一层一层的反序列化逻辑,需要在外层结构体上也实现 UnmarshalJSON。具体实现可参考下方:

type Outer struct {
	Inner
	K any `json:"k"`
}

func (o *Outer) UnmarshalJSON(b []byte) error {
  // unmarshal into inner
	if err := json.Unmarshal(b, &o.Inner); err != nil {
		return err
	}
  // unmarshal self
	type Alias *Outer
	if err := json.Unmarshal(b, (Alias)(o)); err != nil {
		return err
	}
	return nil
}

更进一步,如果是更多级的嵌套类型,那么就要求每一层都实现一遍 UnmarshalJSON。可以说最里面的一个类型一旦实现了 UnmarshalJSON,是牵一发而动全身。如果不加注意,就可能造成非常严重的 BUG。

所以,很多情况下是其实并不推荐使用内嵌类型,尽可能显式地定义字段名称,可以避免未来不小心掉入陷阱。