cover

Go言語の型埋め込みがデシリアライズにおいて持つ巧妙な利用と落とし穴

sorcererxw

Go言語における strcut embeding は、オブジェクト指向における extends を代替するシンタックスシュガーと考えられることが多いです。オブジェクト指向のモデリング作業を簡単に実現するだけでなく、この方法を使ってフィールドやメソッドをオーバーライドすることは、JSONの逆シリアライズにおいてもいくつかの巧妙なテクニックがあります。

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

フィールドタイプの置換

型の内嵌を使用して内嵌型の中のフィールドの型をオーバーライドすることができ、典型的な使用シーンは次の通りです:

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.TimeRFC3339形式のタイムスタンプのみを逆シリアライズできますが、Unixタイムスタンプを逆シリアライズする必要がある場合はどうすればいいでしょうか?カスタムInner構造体のUnmarshalJSONメソッドを定義し、その中でOuter構造体を使用してCreateTimeフィールドを上書きすることができます。このようにして逆シリアライズの過程で、timeフィールドが解析されると、値をOuter.CreateTimeフィールドに書き込もうと試み、Inner.CreateTimeを迂回します。

ただし、注意すべき点があります。Outerの中でInner型をAlias型に置き換える必要があります。そうしないと、シリアライズの過程で、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を実装すると、一つの小さな変更が全体に影響を及ぼすことになります。注意を払わないと、非常に深刻なバグを引き起こす可能性があります。

したがって、多くの場合、内蔵型を使用することは実際には推奨されておらず、将来うっかり罠に落ちることを避けるためにも、可能な限り明示的にフィールド名を定義することが望ましいです。