本記事でやること
-
{"name":"Alice"}という単純な JSON をPerson{Name: "Alice"}構造体に unmarshal する処理の内部実装を追っていきます
対象読者
- encoding/json(v1, v2)を使ったことがある方
- Go標準ライブラリの内部実装に興味がある方
はじめに
本記事はGo1.25で実験的に追加されたencoding/json/v2パッケージのUnmarshal関数の内部実装を追っていくシリーズの第3回です。
第1回: パッケージ設計とメモリ効率編
第2回: JSONの構文検証編
今回は{"name":"Alice"} という単純な JSON を構造体に unmarshal する処理がどのように実行されるのかをステップバイステップで追っていきます。
1. 型に応じたunmarshal関数の取得 - lookupArshaler
lookupArshaler(t)は型毎のunmarshal関数を返します。構造体の場合は、makeStructArshalerで生成された関数が使われます。
lookupArshaler関数は、arshaler構造体を返します。arshaler構造体はunmarshal関数を保持しています。
また、lookupArshaler関数内ではlookupArshalerCache(sync.Map型)によってarshaler構造体のキャッシュを持っています。そのため、キャッシュがあれば即座にreturnし、なければ型に応じたarshaler構造体を作成します。
makeDefaultArshaler関数で型に応じたarshaler構造体を作成します。今回の例では{"name":"Alice"}を構造体にunmarshalするので、reflect.Structのcase文に入りmakeStructArshaler関数によってunmarshal関数が生成されます。
2. unmarshal関数の生成 - makeStructArshaler
makeStructArshalerは構造体型に対するunmarshal関数を生成します。生成される関数の処理フローを追っていきます。
2.1 最初のトークンを読む
jsonの最初のトークンをReadToken関数によって読み取ります。{"name":"Alice"}の場合、最初のトークンは{になります。
2.2 jsonオブジェクトメンバーのループ処理
実際のループ処理は上記の通りですが、要所をかいつまんで紹介します。
for dec.PeekKind() != '}' {
// 1. オブジェクトメンバー名を読む
val, err := xd.ReadValue(&flags)
name := jsonwire.UnquoteMayCopy(val, flags.IsVerbatim())
// 2. フィールドを検索
f := fields.byActualName[string(name)]
// 3. 値の unmarshal 関数を取得
unmarshal := f.fncs.unmarshal
// 4. フィールド値への参照を取得
v := addressableValue{va.Field(f.index0), va.forcedAddr}
// 5. 値を unmarshal(再帰呼び出し)
err = unmarshal(dec, v, uo)
}
{"name":"Alice"} の処理フロー
ループ1回目:
1. ReadValue() → "name" を読む
2. fields.byActualName["name"] → Name フィールド発見
3. f.fncs.unmarshal → 文字列用 unmarshal 関数を取得
4. va.Field(0) → Person.Name への参照
5. unmarshal() → "Alice" を読んで Name にセット
ループ終了:
PeekKind() → '}' を検出 → ループ終了
2.3 フィールド検索の詳細
// 完全一致を試行
f := fields.byActualName[string(name)]
// 見つからなければ大文字小文字を無視してマッチング
if f == nil {
for _, f2 := range fields.lookupByFoldedName(name) {
if f2.matchFoldedName(name, &uo.Flags) {
f = f2
break
}
}
}
// それでも見つからない場合
if f == nil {
if uo.Flags.Get(jsonflags.RejectUnknownMembers) {
return newUnmarshalErrorAfter(dec, t, ErrUnknownName)
}
// unknown フィールドはスキップ
dec.SkipValue()
continue
}
3. 再帰的なunmarshla - 文字列フィールドの処理
今回の例({"name":"Alice"})ではNameフィールドの型に対応するunmarshal関数を取得します。
Nameフィールドはstring型なので、makeStringArshalerで生成された関数が再帰的に呼ばれます。
再帰処理の全体像
makeStructArshaler.unmarshal (構造体用)
↓
フィールド "name" を発見
↓
f.fncs.unmarshal を呼び出し
↓
makeStringArshaler.unmarshal (文字列用)
↓
"Alice" を読んで va.SetString("Alice")
4. ReadValueの内部 - 4セグメントバッファ
ReadValue は JSON 値を読み取る低レベル関数です。v2 では効率的な4セグメントバッファを採用しています。
buf[0 cap(buf)]
│ │
├─────────┬─────────┬───────┬───────┤
│ already │ prev │unread │unused │
│ read │ value │ │ │
└─────────┴─────────┴───────┴───────┘
0 prevStart prevEnd len cap
Segment 1: buf[0:prevEnd] - 既読部分
Segment 2: buf[prevStart:prevEnd] - 直前に読んだ値(ゼロコピーでアクセス可能)
Segment 3: buf[prevEnd:len(buf)] - 未読部分
Segment 4: buf[len(buf):cap(buf)] - 未使用領域
この設計の利点:
- 直前に読んだ値を再コピーなしで参照できる
- ストリーミング処理時にバッファを効率的に再利用
処理フロー全体図
入力: {"name":"Alice"}
Unmarshal()
↓
unmarshalDecode()
↓
lookupArshaler(Person) → makeStructArshaler で生成された関数
↓
┌─────────────────────────────────────────────────────┐
│ struct unmarshal 関数 │
│ │
│ 1. ReadToken() → '{' を読む │
│ ↓ │
│ 2. ループ開始 (PeekKind() != '}') │
│ ↓ │
│ 3. ReadValue() → "name" を読む │
│ ↓ │
│ 4. fields.byActualName["name"] → フィールド発見 │
│ ↓ │
│ 5. f.fncs.unmarshal を取得 (string用) │
│ ↓ │
│ 6. va.Field(0) → Name フィールドへの参照 │
│ ↓ │
│ ┌─────────────────────────────────────────────┐ │
│ │ string unmarshal 関数(再帰呼び出し) │ │
│ │ │ │
│ │ ReadValue() → "Alice" を読む │ │
│ │ UnquoteMayCopy() → 引用符を外す │ │
│ │ va.SetString("Alice") │ │
│ └─────────────────────────────────────────────┘ │
│ ↓ │
│ 7. PeekKind() → '}' を検出 → ループ終了 │
│ ↓ │
│ 8. ReadToken() → '}' を読んで完了 │
└─────────────────────────────────────────────────────┘
↓
結果: Person{Name: "Alice"}
まとめ
v2 の makeStructArshaler による unmarshal 処理は以下の流れで実行されます
-
関数取得:
lookupArshaler()で型に応じた unmarshal 関数を取得 -
オブジェクト処理:
{を読んでループ開始 - フィールドマッチング: JSON キーと構造体フィールドを対応付け
- 再帰的処理: 各フィールドの型に応じた unmarshal 関数を呼び出し
-
値の設定:
reflect.Value.SetXxx()でフィールドに値を設定
v2 の特徴である4セグメントバッファや正確なエラー位置報告は、高パフォーマンスと良好なデバッグ体験を両立させています。