[インデックス 16152] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json
パッケージにおける匿名フィールド(埋め込みフィールド)の処理に関するバグ修正と改善を目的としています。特に、JSONエンコーディング時に構造体の匿名フィールドがどのように扱われるか、その選択ロジックが修正されています。
コミット
commit 357e37dc945885a141b48182c4606f1aac8320db
Author: Rob Pike <r@golang.org>
Date: Tue Apr 9 15:00:21 2013 -0700
encoding/json: fix handling of anonymous fields
The old code was incorrect and also broken. It passed the tests by accident.
The new algorithm is:
1) Sort the fields in order of names.
2) For all fields with the same name, sort in increasing depth.
3) Choose the single field with shortest depth.
If any of the fields of a given name has a tag, do the above using
tagged fields of that name only.
Fixes #5245.
R=iant
CC=golang-dev
https://golang.org/cl/8583044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/357e37dc945885a141b48182c4606f1aac8320db
元コミット内容
このコミットは、encoding/json
パッケージにおける匿名フィールドの処理に関する既存のコードが不正確であり、かつ壊れていた点を修正しています。以前のコードは偶然テストを通過していただけで、正しい動作を保証していませんでした。
新しいアルゴリズムは以下の通りです。
- フィールドを名前の順にソートします。
- 同じ名前を持つすべてのフィールドについて、深さ(埋め込みの階層)が浅い順にソートします。
- 最も深さが浅い単一のフィールドを選択します。
- もし、特定の名前を持つフィールドの中にJSONタグが付けられたものがある場合、上記のルールはタグ付きフィールドのみに適用されます。
この修正は、Go Issue #5245 を解決します。
変更の背景
Go言語の構造体には、匿名フィールド(埋め込みフィールド)という強力な機能があります。これにより、ある構造体の中に別の構造体を埋め込むことで、その埋め込まれた構造体のフィールドやメソッドをあたかも自身のフィールドやメソッドであるかのようにアクセスできます。これは、コードの再利用性やインターフェースの実装において非常に便利です。
しかし、この匿名フィールドの解決ルールは複雑であり、特に同じ名前のフィールドが異なる深さで存在する場合や、JSONタグの有無によってその挙動が変わる場合があります。encoding/json
パッケージは、Goの構造体をJSONにエンコード(Marshal)する際に、これらの匿名フィールドのルールに従ってどのフィールドをJSON出力に含めるかを決定する必要があります。
このコミット以前の encoding/json
パッケージの実装では、匿名フィールドの解決ロジックに誤りがありました。具体的には、同じ名前のフィールドが複数存在する場合に、Go言語の仕様に則った正しいフィールドの選択が行われていませんでした。コミットメッセージにある「The old code was incorrect and also broken. It passed the tests by accident.」という記述は、この問題の深刻さを示しています。つまり、既存のテストケースではたまたま問題が顕在化しなかっただけで、潜在的に誤ったJSON出力が生成される可能性があったということです。
この問題は、Go Issue #5245 として報告されており、このコミットはその問題を解決するために導入されました。背景には、Go言語の構造体埋め込みのセマンティクスと、それをJSONエンコーディングに正確に反映させる必要性がありました。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と encoding/json
パッケージの基本的な動作に関する知識が必要です。
1. Go言語の構造体と匿名フィールド(埋め込みフィールド)
Go言語の構造体は、異なる型のフィールドをまとめるための複合データ型です。匿名フィールドは、構造体の中にフィールド名なしで別の型(通常は別の構造体)を埋め込む機能です。
例:
type Person struct {
Name string
Age int
}
type Employee struct {
Person // 匿名フィールド
ID string
}
func main() {
e := Employee{
Person: Person{Name: "Alice", Age: 30},
ID: "E123",
}
fmt.Println(e.Name) // Alice (PersonのNameフィールドに直接アクセスできる)
}
2. フィールドの昇格(Promotion)
匿名フィールドが埋め込まれると、その匿名フィールドのフィールドやメソッドは、外側の構造体のフィールドやメソッドであるかのように「昇格」します。これにより、上記のように e.Name
のように直接アクセスできるようになります。
3. フィールドの衝突解決ルール
Go言語では、同じ名前のフィールドが複数存在する場合(特に匿名フィールドを通じて)、以下のルールで解決されます。
- 直接定義されたフィールドの優先: 構造体自身に直接定義されたフィールドは、匿名フィールドを通じて昇格した同名のフィールドよりも優先されます。
- 深さの優先: 匿名フィールドを通じて昇格したフィールドが複数ある場合、より「浅い」深さ(つまり、埋め込み階層が少ない)にあるフィールドが優先されます。
- 曖昧さの排除: もし同じ深さで同じ名前のフィールドが複数存在する場合、それは曖昧な参照となり、コンパイルエラーになります。
例:
type A struct {
X int
}
type B struct {
A
X int // Bに直接定義されたX
}
type C struct {
A
Y int
}
type D struct {
C
A // Dに匿名でAが埋め込まれている
}
// BのXはB自身のXが優先される
// DのXはD.C.A.XとD.A.Xで曖昧になる(コンパイルエラー)
4. encoding/json
パッケージとJSONタグ
encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換を行うための標準ライブラリです。構造体をJSONにエンコードする際、フィールドの隣にバッククォートで囲まれた「JSONタグ」を付けることで、JSONでのフィールド名を変更したり、フィールドを無視したりするなどの制御が可能です。
例:
type User struct {
Name string `json:"user_name"` // JSONでは"user_name"として出力
Age int `json:"-"` // JSON出力から除外
Email string // JSONでは"Email"として出力
}
JSONタグは、匿名フィールドの解決ルールにも影響を与えます。特に、タグが付けられたフィールドは、タグがない同名のフィールドよりも優先される場合があります。
5. reflect
パッケージ
encoding/json
パッケージは、Goの型情報を実行時に検査するために reflect
パッケージを内部的に使用しています。構造体のフィールド名、型、タグなどの情報を取得し、それに基づいてJSONエンコーディングのロジックを適用します。
技術的詳細
このコミットの主要な変更は、src/pkg/encoding/json/encode.go
ファイル内の typeFields
関数と、新しく追加された dominantField
関数に集約されています。これらの関数は、Goの構造体からJSONエンコーディングの対象となるフィールドのリストを構築する役割を担っています。
typeFields
関数の変更点
typeFields
関数は、与えられた reflect.Type
(構造体の型)から、JSONエンコーディングの対象となるすべてのフィールドを抽出し、適切な順序でソートして返します。
変更前: 変更前のコードは、フィールドを名前でソートした後、単純なループで「衝突する名前を持つフィールド」や「明示的なJSONタグを持つフィールドによってシャドウされるフィールド」を削除しようとしていました。このロジックは、Goの匿名フィールドの昇格ルールや衝突解決ルールを正確に反映できていませんでした。特に、深さの概念やタグの優先順位が適切に考慮されていませんでした。
// 変更前の簡略化されたロジックのイメージ
// sort.Sort(byName(fields))
// out := fields[:0]
// for _, f := range fields {
// if f.name != name { // 新しい名前のフィールド
// out = append(out, f)
// name = f.name
// continue
// }
// // 同じ名前のフィールドの場合、タグの有無で上書きを試みるが不完全
// if n := len(out); n > 0 && out[n-1].name == name && (!out[n-1].tag || f.tag) {
// out = out[:n-1] // 既存のフィールドを削除
// }
// // ここでfを追加するロジックが複雑で、深さの考慮が不十分
// }
変更後:
変更後の typeFields
関数は、より堅牢なアルゴリズムを採用しています。
- 名前によるソート: まず、すべてのフィールドを名前でソートします。これは変更前と同じです。
- 名前ごとのグループ化と
dominantField
の呼び出し:typeFields
は、ソートされたフィールドリストを名前ごとにグループ化してイテレートします。- 同じ名前を持つフィールドのグループ(
fields[i:i+advance]
)が見つかると、新しく導入されたdominantField
関数を呼び出します。 dominantField
関数は、このグループの中からGoのルールに従って「支配的な(dominant)」フィールドを一つだけ選択します。dominantField
が選択したフィールド(または衝突により選択されなかった場合は空のフィールド)がout
スライスに追加されます。
- インデックスによる最終ソート: 最後に、選択されたフィールドを元の構造体におけるフィールドのインデックス(定義順)でソートし直します。これは、JSON出力の順序を安定させるためです。
この新しいアプローチにより、匿名フィールドの複雑な解決ルール(深さの優先、タグの優先)が dominantField
関数にカプセル化され、typeFields
のロジックがより明確になりました。
dominantField
関数の追加
dominantField
関数は、このコミットの核心となる新しいロジックを実装しています。この関数は、同じ名前を持つ複数のフィールド(匿名フィールドを通じて昇格したものを含む)を受け取り、Goの埋め込みルールとJSONタグの存在を考慮して、JSONエンコーディングの対象となる唯一のフィールドを決定します。
引数:
fields []field
: 同じ名前を持つフィールドのリスト。hasTags bool
: このリスト内のいずれかのフィールドにJSONタグが付けられているかどうかを示すブール値。
ロジック:
-
JSONタグの優先:
- もし
hasTags
がtrue
であれば、つまり、この名前のフィールドの中に一つでもJSONタグが付けられたものがある場合、タグのないフィールドはすべて無視されます。これは、JSONタグが付けられたフィールドが、タグのない同名のフィールドよりも優先されるというGoのencoding/json
の特殊なルールを反映しています。 - このステップの後、
fields
スライスはタグ付きフィールドのみを含むようにフィルタリングされます。
- もし
-
深さの優先と曖昧さのチェック:
fields
スライスは、typeFields
の呼び出し元で既に「インデックスの長さ」(つまり、埋め込みの深さ)が短い順にソートされています。- したがって、
fields[0]
は最も深さが浅い(または直接定義された)フィールドです。 len(fields) > 1 && len(fields[0].index) == len(fields[1].index)
の条件は、残ったフィールドが複数あり、かつ最も浅い深さのフィールドが複数存在する場合(つまり、同じ深さで同じ名前のフィールドが衝突している場合)をチェックします。- Goのルールでは、このような曖昧な衝突はエラーと見なされ、これらのフィールドはJSONエンコーディングの対象から除外されます。この場合、
dominantField
は(field{}, false)
を返します。 - それ以外の場合(衝突がない場合)、
fields[0]
が支配的なフィールドとして選択され、(fields[0], true)
が返されます。
この dominantField
関数により、Goの複雑な匿名フィールドの解決ルールが正確に encoding/json
パッケージに適用されるようになりました。
テストファイルの変更
src/pkg/encoding/json/decode_test.go
: コメントのタイポ修正 (unmarshalling
->unmarshaling
) のみで、機能的な変更はありません。src/pkg/encoding/json/encode_test.go
:TestEmbeddedBug
という新しいテスト関数が追加されました。- このテストは、Issue #5245 で報告された具体的なバグケースを再現し、修正後の
encoding/json
が正しく動作することを確認します。 BugB
構造体(BugA
を匿名で埋め込み、かつ自身のS
フィールドを持つ)とBugD
構造体(BugA
とBugB
を匿名で埋め込み、かつ自身のA
フィールドを持つ)が定義されています。BugB
のケースでは、BugA
のS
とBugB
自身のS
が衝突しますが、GoのルールによりBugB
自身のS
が優先されることを確認します。BugD
のケースでは、BugA
とBugB
の両方からS
フィールドが昇格しますが、Goのルールではこれらが曖昧な参照となり、JSON出力には含まれないことを確認します。
コアとなるコードの変更箇所
このコミットの主要な変更は、以下のファイルに集中しています。
src/pkg/encoding/json/encode.go
:typeFields
関数のロジックが大幅に書き換えられました。特に、匿名フィールドの解決と衝突処理に関する部分が変更されています。dominantField
という新しいヘルパー関数が追加されました。この関数が匿名フィールドの解決ロジックの大部分を担っています。
src/pkg/encoding/json/encode_test.go
:TestEmbeddedBug
という新しいテストケースが追加され、匿名フィールドの処理に関するバグが修正されたことを検証しています。
具体的な変更行数:
src/pkg/encoding/json/encode.go
: 63行追加, 6行削除src/pkg/encoding/json/encode_test.go
: 50行追加src/pkg/encoding/json/decode_test.go
: 1行追加, 1行削除 (コメント修正のみ)
コアとなるコードの解説
src/pkg/encoding/json/encode.go
typeFields
関数 (変更後)
func typeFields(t reflect.Type) []field {
// ... (既存のフィールド抽出ロジック) ...
sort.Sort(byName(fields)) // フィールドを名前でソート
// Delete all fields that are hidden by the Go rules for embedded fields,
// except that fields with JSON tags are promoted.
//
// The fields are sorted in primary order of name, secondary order
// of field index length. Loop over names; for each name, delete
// hidden fields by choosing the one dominant field that survives.
out := fields[:0] // 結果を格納するスライス
for advance, i := 0, 0; i < len(fields); i += advance {
// One iteration per name.
// Find the sequence of fields with the name of this first field.
fi := fields[i]
name := fi.name
hasTags := fi.tag // この名前のフィールドにタグがあるかどうかのフラグ
for advance = 1; i+advance < len(fields); advance++ {
fj := fields[i+advance]
if fj.name != name {
break // 異なる名前のフィールドに到達
}
hasTags = hasTags || fj.tag // 同じ名前のフィールドの中にタグ付きがあればフラグを立てる
}
if advance == 1 { // この名前のフィールドが一つしかない場合
out = append(out, fi)
continue
}
// 同じ名前のフィールドが複数ある場合、dominantFieldで一つに絞る
dominant, ok := dominantField(fields[i:i+advance], hasTags)
if ok {
out = append(out, dominant)
}
}
fields = out // フィルタリングされたフィールドリストを更新
sort.Sort(byIndex(fields)) // 最終的に元の構造体のフィールド順にソート
return fields
}
この変更により、typeFields
はまずフィールドを名前でソートし、その後、同じ名前を持つフィールドのグループごとに dominantField
を呼び出すようになりました。これにより、Goの埋め込みルールとJSONタグの優先順位が正確に適用され、JSONエンコーディングの対象となる「正しい」フィールドが選択されます。
dominantField
関数 (新規追加)
// dominantField looks through the fields, all of which are known to
// have the same name, to find the single field that dominates the
// others using Go's embedding rules, modified by the presence of
// JSON tags. If there are multiple top-level fields, the boolean
// will be false: This condition is an error in Go and we skip all
// the fields.
func dominantField(fields []field, hasTags bool) (field, bool) {
if hasTags {
// If there's a tag, it gets promoted, so delete all fields without tags.
var j int
for i := 0; i < len(fields); i++ {
if fields[i].tag {
fields[j] = fields[i]
j++
}
}
fields = fields[:j] // タグ付きフィールドのみを残す
}
// The fields are sorted in increasing index-length order. The first entry
// therefore wins, unless the second entry is of the same length. If that
// is true, then there is a conflict (two fields named "X" at the same level)
// and we have no fields.
if len(fields) > 1 && len(fields[0].index) == len(fields[1].index) {
return field{}, false // 曖昧な衝突の場合、フィールドを返さない
}
return fields[0], true // 最も浅い深さのフィールド(または唯一のフィールド)を返す
}
dominantField
は、同じ名前のフィールドのリストを受け取り、Goの埋め込みルールとJSONタグの優先順位に基づいて、JSONエンコーディングに含めるべき単一のフィールドを決定します。
hasTags
がtrue
の場合、タグのないフィールドはすべて破棄されます。- 残ったフィールドの中で、
fields[0]
が最も浅い深さのフィールドです。 - もし
fields[0]
とfields[1]
が存在し、かつ同じ深さ(len(fields[0].index) == len(fields[1].index)
)であれば、それはGoのルールにおける曖昧な衝突であり、どのフィールドも選択されません(false
を返す)。 - それ以外の場合、
fields[0]
が選択されます。
src/pkg/encoding/json/encode_test.go
TestEmbeddedBug
関数 (新規追加)
type BugA struct {
S string
}
type BugB struct {
BugA
S string
}
type BugC struct {
S string
}
// Legal Go: We never use the repeated embedded field (S).
type BugD struct {
A int
BugA
BugB
}
// Issue 5245.
func TestEmbeddedBug(t *testing.T) {
v := BugB{
BugA{"A"},
"B",
}
b, err := Marshal(v)
if err != nil {
t.Fatal("Marshal:", err)
}
want := `{"S":"B"}`
got := string(b)
if got != want {
t.Fatalf("Marshal: got %s want %s", got, want)
}
// Now check that the duplicate field, S, does not appear.
x := BugD{
A: 23,
}
b, err = Marshal(x)
if err != nil {
t.Fatal("Marshal:", err)
}
want = `{"A":23}`
got = string(b)
if got != want {
t.Fatalf("Marshal: got %s want %s", got, want)
}
}
このテストは、BugB
のケースで BugA
の S
と BugB
自身の S
が衝突する際に、BugB
自身の S
が優先されることを確認します(出力は {"S":"B"}
)。
また、BugD
のケースでは、BugA
と BugB
の両方から S
フィールドが昇格し、これらが曖昧な参照となるため、JSON出力には S
フィールドが含まれないことを確認します(出力は {"A":23}
)。これは、Goの埋め込みルールにおける曖昧さの排除が encoding/json
で正しく処理されることを示しています。
関連リンク
- Go Issue #5245: https://github.com/golang/go/issues/5245
- Go CL 8583044: https://golang.org/cl/8583044 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)
参考にした情報源リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/jsonreflect
パッケージのドキュメント: https://pkg.go.dev/reflect- Go言語の構造体埋め込みに関する解説記事 (一般的な情報源):
- A Tour of Go - Embedded fields: https://go.dev/tour/methods/10
- Go by Example - Structs: https://gobyexample.com/structs
- The Go Programming Language Specification - Struct types: https://go.dev/ref/spec#Struct_types (特に "A field declared with a type but no explicit field name is an anonymous field." のセクション)
- Go言語の埋め込みフィールドのルールに関する詳細な解説 (例: "Go's Rules for Struct Embedding"): Web検索で「Go struct embedding rules」などで検索すると多数の記事が見つかります。
- Go言語のJSONタグに関する解説記事 (一般的な情報源): Web検索で「Go json tag」などで検索すると多数の記事が見つかります。