Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 15018] ファイルの概要

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける重要な修正と改善を含んでいます。主な目的は、以前のコミットで導入された、プリミティブ型のJSONアンマーシャリングを高速化する変更が、不正な形式のJSONデータに対する Unmarshal の挙動を破壊した問題を修正することです。具体的には、その高速化のためのコードをロールバックし、同時にプリミティブ型のアンマーシャリング性能を測定するための新しいベンチマークテストを追加し、さらに不正なJSONデータに対する Unmarshal の構文エラーハンドリングを検証するテストケースを拡充しています。これにより、encoding/json パッケージの堅牢性と信頼性が向上しました。

コミット

commit ad37081b672a22f573f91aca7a5828e2c9718314
Author: Russ Cox <rsc@golang.org>
Date:   Tue Jan 29 13:34:18 2013 -0800

    encoding/json: add test for Unmarshal of malformed data
    
    Roll back CL making primitive type unmarshal faster,
    because it broke the Unmarshal of malformed data.
    
    Add benchmarks for unmarshal of primitive types.
    
    Update #3949.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7228061

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ad37081b672a22f573f91aca7a5828e2c9718314

元コミット内容

このコミットの元々の意図は、encoding/json パッケージの Unmarshal 関数において、不正な形式のJSONデータに対するテストを追加することでした。しかし、その背景には、以前に導入された「プリミティブ型のアンマーシャリングを高速化する変更」が、不正なデータに対する Unmarshal の挙動を壊してしまったという問題がありました。そのため、このコミットでは、その問題を引き起こした高速化の変更を元に戻し(ロールバック)、同時にプリミティブ型のアンマーシャリング性能を測定するためのベンチマークテストを追加しています。また、GoのIssue #3949 に関連する更新も行われています。

変更の背景

この変更の背景には、Go言語の encoding/json パッケージにおけるパフォーマンスと堅牢性のバランスに関する課題がありました。

  1. パフォーマンス最適化の失敗: 以前のコミット(Go CL 7228061、このコミットでロールバックされる変更)では、JSONのプリミティブ型(文字列、数値、真偽値など)のアンマーシャリング処理を高速化するための最適化が試みられました。具体的には、JSONデータがオブジェクト({)や配列([)で始まらない場合に、より軽量な処理パスで直接プリミティブ型としてデコードしようとするロジックが Unmarshal 関数に追加されました。
  2. 不正なデータ処理の破壊: しかし、この最適化は、JSONの仕様に準拠しない「不正な形式のデータ」(malformed data)が入力された場合に、予期せぬ挙動や誤ったエラーを返す問題を引き起こしました。例えば、"hello のように閉じられていない文字列や、tru のように不完全な真偽値などが入力された際に、本来 SyntaxError を返すはずが、異なるエラーになったり、クラッシュしたりする可能性がありました。
  3. Issue #3949: この問題は、GoのIssueトラッカーで #3949 として報告されていました。このIssueは、encoding/json パッケージが不正なJSON入力に対して適切に SyntaxError を返さない、または誤ったエラーを返すというバグを指摘していました。
  4. ロールバックとテストの追加: 問題の原因が特定されたため、このコミットでは、問題を引き起こした高速化のロジックを decode.go から完全に削除することでロールバックしています。そして、同様の問題が将来再発しないように、不正なJSONデータに対する SyntaxError のテストケースを decode_test.go に追加しました。さらに、プリミティブ型のアンマーシャリング性能を継続的に監視できるよう、bench_test.go に新しいベンチマークを追加しています。

この一連の変更により、encoding/json パッケージは、パフォーマンスを追求しつつも、JSON仕様に厳密に従い、不正な入力に対しても堅牢なエラーハンドリングを行うという、より信頼性の高い状態に戻されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびJSONに関する基本的な知識が必要です。

  1. JSON (JavaScript Object Notation):

    • 軽量なデータ交換フォーマット。人間が読み書きしやすく、機械が解析しやすい構造を持っています。
    • データ型には、オブジェクト({})、配列([])、文字列("")、数値(1233.14)、真偽値(truefalse)、null があります。
    • 「プリミティブ型」とは、オブジェクトや配列以外の単一の値を指します(文字列、数値、真偽値、null)。
    • 「不正な形式のデータ (malformed data)」とは、JSONの仕様に準拠しない構文エラーを含むデータを指します。例えば、引用符が閉じられていない文字列、数値の途中に不正な文字が含まれる、truetru となっている、などです。
  2. Go言語の encoding/json パッケージ:

    • Goの標準ライブラリで、JSONデータのエンコード(Goの構造体からJSONへ)とデコード(JSONからGoの構造体へ)を提供します。
    • json.Unmarshal(data []byte, v interface{}) error: この関数は、JSON形式のバイトスライス data をGoの任意の型の値 v にデコード(アンマーシャリング)します。v はポインタである必要があります。デコードに成功すれば nil を返し、失敗すればエラーを返します。
    • *json.UnmarshalTypeError: Unmarshal が返す可能性のあるエラー型の一つで、JSONのデータ型とGoのターゲットのデータ型が一致しない場合に発生します(例: JSONの数値 123 をGoの string 型にデコードしようとした場合)。
    • *json.SyntaxError: Unmarshal が返す可能性のあるエラー型の一つで、入力されたJSONデータがJSONの構文規則に違反している場合に発生します。これは、不正な形式のデータ(malformed data)に対する主要なエラーです。
    • reflect パッケージ: Goのランタイムリフレクション機能を提供します。encoding/json のような汎用的なデータ処理ライブラリでは、入力された interface{} の具体的な型情報を実行時に取得・操作するために reflect パッケージが頻繁に利用されます。削除されたコードブロックでも reflect.ValueOf(v) などが使われていました。
  3. Go言語のテストとベンチマーク:

    • testing パッケージ: Goの標準テストフレームワークです。
    • テスト関数: func TestXxx(t *testing.T) の形式で記述され、go test コマンドで実行されます。t.Errorf()t.Fatal() などを使ってテストの失敗を報告します。
    • ベンチマーク関数: func BenchmarkXxx(b *testing.B) の形式で記述され、go test -bench=. コマンドで実行されます。コードの性能を測定するために使用され、b.N 回のループで処理を実行し、その平均実行時間やメモリ割り当てなどを計測します。b.SetBytes() は、ベンチマーク対象の操作が処理したバイト数を記録するために使用されます。

これらの知識があることで、コミットがなぜ行われたのか、どのような技術的課題を解決しようとしているのか、そしてコードの変更が具体的に何を意味するのかを深く理解することができます。

技術的詳細

このコミットの技術的詳細を掘り下げると、encoding/json パッケージの Unmarshal 関数がどのようにJSONデータを解析し、Goの型にマッピングするかの内部的な挙動、そしてパフォーマンス最適化とエラーハンドリングの間のトレードオフが浮き彫りになります。

ロールバックされた最適化のメカニズムと問題点

src/pkg/encoding/json/decode.go から削除されたコードブロックは、以下のようなロジックを持っていました。

// skip heavy processing for primitive values
var first byte
var i int
for i, first = range data {
	if !isSpace(rune(first)) {
		break
	}
}
if first != '{' && first != '[' {
	rv := reflect.ValueOf(v)
	if rv.Kind() != reflect.Ptr || rv.IsNil() {
		return &InvalidUnmarshalError{reflect.TypeOf(v)}
	}
	var d decodeState
	d.literalStore(data[i:], rv.Elem(), false)
	return d.savedError
}

このコードは、入力されたJSONデータ data の先頭から空白文字をスキップし、最初の非空白文字 first をチェックしていました。

  • もし first{ (JSONオブジェクトの開始) でも [ (JSON配列の開始) でもない場合、それはプリミティブ型(文字列、数値、真偽値、null)であると推測していました。
  • この推測が正しければ、通常の複雑なJSONパーシングロジックをスキップし、d.literalStore というより軽量な関数を使って、残りのデータ data[i:] を直接ターゲットのGoの型 rv.Elem() にデコードしようとしていました。

問題点: この最適化は、JSONの構文解析が完全に行われる前に、データの「見た目」だけでプリミティブ型であると判断してしまう点に脆弱性がありました。 例えば、"hello のように引用符が閉じられていない文字列や、tru のように不完全な真偽値、123e のように指数部が不完全な数値など、構文的に不正なプリミティブ型が入力された場合、この高速パスはそれらを「プリミティブ型」として処理しようとします。しかし、literalStore は完全なJSON構文解析器ではないため、これらの不正な形式を正しく認識できず、結果として誤った値にデコードされたり、予期せぬエラー(SyntaxError ではないエラー)を返したり、最悪の場合パニックを引き起こしたりする可能性がありました。

本来、Unmarshal は入力データ全体をJSONとして厳密に解析し、構文エラーがあれば *json.SyntaxError を返す必要があります。この最適化は、その厳密な構文チェックの一部を迂回してしまったため、不正なデータに対する堅牢性が損なわれたのです。

ロールバックによる堅牢性の回復

このコミットでは、上記のコードブロックを decode.go から完全に削除しました。これにより、Unmarshal 関数は、入力データがプリミティブ型であろうと複合型であろうと、常に完全なJSONパーシングロジック(d := new(decodeState).init(data) 以降の処理)を通るようになりました。この変更は、プリミティブ型のアンマーシャリング速度をわずかに犠牲にするかもしれませんが、JSON仕様への厳密な準拠と、不正なデータに対する正確な *json.SyntaxError の返却を保証します。

新しいテストの追加

  1. TestUnmarshalSyntax (decode_test.go):

    • このテストは、unmarshalSyntaxTests という文字列スライスに、意図的に不正な形式のJSON文字列(例: tru, fals, nul, 123e, \"hello, [1,2,3, {\"key\":1, {\"key\":1,)を定義しています。
    • 各不正な文字列に対して Unmarshal を実行し、返されるエラーが必ず *json.SyntaxError であることをアサートしています。
    • これにより、Unmarshal が不正なJSON入力に対して常に正しい種類のエラー(構文エラー)を返すことが保証され、以前の最適化が引き起こした問題の再発を防ぐための強力な回帰テストとなります。
  2. 新しいベンチマーク (bench_test.go):

    • BenchmarkUnmarshalString, BenchmarkUnmarshalFloat64, BenchmarkUnmarshalInt64 の3つの新しいベンチマークが追加されました。
    • これらはそれぞれ、JSON文字列、JSON浮動小数点数、JSON整数をGoの対応するプリミティブ型にアンマーシャリングする際の性能を測定します。
    • これらのベンチマークの追加は、ロールバックによってプリミティブ型のアンマーシャリング性能がどの程度影響を受けたかを定量的に把握するため、そして将来的に同様の最適化を試みる際に、性能改善と堅牢性のバランスを評価するための基準点を提供します。

これらの技術的変更は、Goの encoding/json パッケージが、パフォーマンスだけでなく、正確性と堅牢性も重視する設計哲学を反映していることを示しています。

コアとなるコードの変更箇所

このコミットにおけるコアとなるコードの変更は、以下の3つのファイルに集中しています。

  1. src/pkg/encoding/json/decode.go:

    • 変更内容: Unmarshal 関数から、プリミティブ型のアンマーシャリングを高速化しようとした約20行のコードブロックが完全に削除されました。
    • 具体的に削除されたコード:
      // skip heavy processing for primitive values
      var first byte
      var i int
      for i, first = range data {
          if !isSpace(rune(first)) {
              break
          }
      }
      if first != '{' && first != '[' {
          rv := reflect.ValueOf(v)
          if rv.Kind() != reflect.Ptr || rv.IsNil() {
              return &InvalidUnmarshalError{reflect.TypeOf(v)}
          }
          var d decodeState
          d.literalStore(data[i:], rv.Elem(), false)
          return d.savedError
      }
      
    • 影響: この削除により、すべてのJSON入力は、プリミティブ型であっても、より堅牢な完全なJSONパーシングロジックを通過するようになります。これにより、不正な形式のデータに対する Unmarshal の挙動が修正され、常に適切な *json.SyntaxError が返されるようになります。
  2. src/pkg/encoding/json/bench_test.go:

    • 変更内容: プリミティブ型のアンマーシャリング性能を測定するための新しいベンチマーク関数が3つ追加されました。
    • 追加された関数:
      • BenchmarkUnmarshalString(b *testing.B): JSON文字列 ("hello, world") のアンマーシャリング。
      • BenchmarkUnmarshalFloat64(b *testing.B): JSON浮動小数点数 (3.14) のアンマーシャリング。
      • BenchmarkUnmarshalInt64(b *testing.B): JSON整数 (3) のアンマーシャリング。
    • 影響: これらのベンチマークは、今後のパフォーマンス改善の試みや、回帰テストの基準として機能します。ロールバックによる性能影響を定量的に把握するためにも重要です。
  3. src/pkg/encoding/json/decode_test.go:

    • 変更内容: 不正な形式のJSONデータに対する SyntaxError のテストケースが追加されました。
    • 追加されたコード:
      • unmarshalSyntaxTests という文字列スライス:
        var unmarshalSyntaxTests = []string{
            "tru",
            "fals",
            "nul",
            "123e",
            `"hello`,
            `[1,2,3`,
            `{"key":1`,
            `{"key":1,`,
        }
        
      • TestUnmarshalSyntax(t *testing.T) 関数: この関数は unmarshalSyntaxTests の各要素を Unmarshal に渡し、返されるエラーが *json.SyntaxError であることを検証します。
    • 影響: このテストの追加により、Unmarshal が不正なJSON入力に対して常に正しい SyntaxError を返すことが保証され、以前のバグの再発を防ぐための重要な安全網となります。

これらの変更は、パフォーマンス最適化の試みが引き起こしたバグを修正し、同時に将来の性能評価と堅牢性確保のためのテストインフラを強化するという、コミットの目的を直接的に反映しています。

コアとなるコードの解説

src/pkg/encoding/json/decode.go の変更 (ロールバック)

削除されたコードブロックは、Unmarshal 関数内で、JSONデータがオブジェクトや配列ではない(つまりプリミティブ型である)と判断した場合に、より高速なデコードパスを試みるためのものでした。

// 削除されたコード: プリミティブ値に対する重い処理をスキップする試み
var first byte
var i int
for i, first = range data { // データの先頭から空白をスキップし、最初の非空白文字を見つける
    if !isSpace(rune(first)) {
        break
    }
}
if first != '{' && first != '[' { // 最初の文字が '{' や '[' でなければ、プリミティブ型と仮定
    rv := reflect.ValueOf(v) // ターゲットのGoの型をリフレクションで取得
    if rv.Kind() != reflect.Ptr || rv.IsNil() { // ポインタ型でなければエラー
        return &InvalidUnmarshalError{reflect.TypeOf(v)}
    }
    var d decodeState
    // literalStore を使って、残りのデータを直接プリミティブとしてデコードしようとする
    d.literalStore(data[i:], rv.Elem(), false)
    return d.savedError // デコード結果のエラーを返す
}

このロジックは、JSONの完全な構文解析を行う前に、データの最初の数バイトだけで「プリミティブ型である」と推測し、literalStore という関数で直接デコードを試みていました。literalStore は、true, false, null, 数値、引用符で囲まれた文字列といったJSONリテラルを効率的に解析するための内部関数です。

なぜ問題だったのか: このアプローチは、完全に正しいJSONプリミティブデータに対しては高速に動作する可能性があります。しかし、"hello (閉じられていない文字列) や tru (不完全な真偽値) のような不正な形式のデータが入力された場合、first != '{' && first != '[' の条件は満たされるため、この高速パスに入ってしまいます。しかし、literalStore はJSONの厳密な構文規則全体をチェックするわけではないため、これらの不正な形式を正しく *json.SyntaxError として識別できず、誤ったエラーを返したり、予期せぬ挙動を引き起こしたりしました。

このコミットでは、このコードブロックを削除することで、すべてのJSON入力が、より堅牢で完全なJSONパーシングロロジック(d := new(decodeState).init(data) 以降の処理)を通過するように戻しました。これにより、パフォーマンスはわずかに低下するかもしれませんが、JSON仕様への厳密な準拠と、不正なデータに対する正確な *json.SyntaxError の返却が保証されます。

src/pkg/encoding/json/bench_test.go の変更 (ベンチマークの追加)

追加されたベンチマーク関数は、Goの testing パッケージのベンチマーク機能を利用しています。

func BenchmarkUnmarshalString(b *testing.B) {
    data := []byte(`"hello, world"`) // テストデータ: JSON文字列
    var s string // デコード先のGoの変数
    for i := 0; i < b.N; i++ { // b.N 回ループして処理を実行
        if err := Unmarshal(data, &s); err != nil {
            b.Fatal("Unmarshal:", err) // エラーが発生したらベンチマークを停止
        }
    }
}
// BenchmarkUnmarshalFloat64 と BenchmarkUnmarshalInt64 も同様の構造
  • b *testing.B はベンチマークコンテキストを提供します。
  • b.N は、ベンチマークフレームワークが処理を繰り返す回数です。このループ内で測定対象のコードを実行します。
  • Unmarshal(data, &s) は、JSONバイトスライスをGoの変数にデコードする操作です。
  • これらのベンチマークは、文字列、浮動小数点数、整数の各プリミティブ型が Unmarshal によってどれくらいの時間で処理されるかを測定します。これにより、将来の変更がこれらの基本的なデコード操作の性能に与える影響を定量的に評価できるようになります。

src/pkg/encoding/json/decode_test.go の変更 (構文エラーテストの追加)

追加された unmarshalSyntaxTestsTestUnmarshalSyntax 関数は、不正なJSONデータに対する Unmarshal のエラーハンドリングを検証します。

var unmarshalSyntaxTests = []string{
    "tru",      // 不完全な真偽値
    "fals",     // 不完全な真偽値
    "nul",      // 不完全なnull
    "123e",     // 不完全な数値 (指数部)
    `"hello`,   // 閉じられていない文字列
    `[1,2,3`,   // 閉じられていない配列
    `{"key":1`, // 閉じられていないオブジェクト
    `{"key":1,`,// 不完全なオブジェクト (末尾のカンマ)
}

func TestUnmarshalSyntax(t *testing.T) {
    var x interface{} // 任意の型にデコードを試みる
    for _, src := range unmarshalSyntaxTests { // 各不正なJSON文字列に対して
        err := Unmarshal([]byte(src), &x) // Unmarshal を実行
        if _, ok := err.(*SyntaxError); !ok { // 返されたエラーが *json.SyntaxError でない場合
            t.Errorf("expected syntax error for Unmarshal(%q): got %T", src, err) // テスト失敗
        }
    }
}
  • unmarshalSyntaxTests には、JSONの構文規則に違反する様々なパターンが列挙されています。
  • TestUnmarshalSyntax は、これらの不正な文字列を Unmarshal に渡し、返されるエラーが *json.SyntaxError 型であることを厳密にチェックします。
  • このテストは、以前の最適化が引き起こした「不正なデータに対して正しいエラーを返さない」というバグを直接的に検証し、修正後の Unmarshal が常にJSONの構文エラーを正確に報告することを保証します。これは、encoding/json パッケージの堅牢性を維持するために非常に重要な回帰テストです。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント encoding/json パッケージ: https://pkg.go.dev/encoding/json
  • Go言語公式ドキュメント testing パッケージ: https://pkg.go.dev/testing
  • JSONの公式ウェブサイト: https://www.json.org/json-en.html
  • Go言語におけるリフレクション (reflect パッケージ) の概念: https://pkg.go.dev/reflect (削除されたコードブロックの理解に役立つ)
  • Go言語のベンチマークの書き方に関する一般的な情報 (例: Goのブログ記事など)
  • Go言語のIssueトラッカーの利用方法に関する一般的な情報