1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Golang]encoding/json/v2のUnmarshal関数の内部実装を追ってみる(3)~構造体フィールドへのマッピング~

1
Posted at

本記事でやること

  • {"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 処理は以下の流れで実行されます

  1. 関数取得: lookupArshaler() で型に応じた unmarshal 関数を取得
  2. オブジェクト処理: { を読んでループ開始
  3. フィールドマッチング: JSON キーと構造体フィールドを対応付け
  4. 再帰的処理: 各フィールドの型に応じた unmarshal 関数を呼び出し
  5. 値の設定: reflect.Value.SetXxx() でフィールドに値を設定

v2 の特徴である4セグメントバッファや正確なエラー位置報告は、高パフォーマンスと良好なデバッグ体験を両立させています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?