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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおける Unmarshal 関数の最適化をロールバックし、関連するバグを修正するためのテストを追加するものです。具体的には、以前導入された Unmarshal の最適化がパニックを引き起こす可能性があったため、その最適化を元に戻し、その問題を再現するテストケースを追加しています。

コミット

commit d340a89d9c872438841e9f3ff90e6baa6fa3d8ce
Author: Russ Cox <rsc@golang.org>
Date:   Thu Feb 14 14:46:15 2013 -0500

    encoding/json: roll back Unmarshal optimization + test
    
    The second attempt at the Unmarshal optimization allowed
    panics to get out of the json package. Add test for that bug
    and remove the optimization.
    
    Let's stop trying to optimize Unmarshal.
    
    Fixes #4784.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7300108

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

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

元コミット内容

このコミットは、encoding/json パッケージの Unmarshal 関数に加えられた以前の最適化を元に戻すものです。具体的には、JSON文字列がオブジェクト({...})や配列([...])ではない場合に、より軽量な処理で済ませようとする試みでした。しかし、この「2回目の最適化の試み」が json パッケージの外部にパニックを漏洩させる原因となったため、その最適化を完全に削除し、そのバグを捕捉するためのテストケースを追加しています。コミットメッセージには「Let's stop trying to optimize Unmarshal.(Unmarshalの最適化を試みるのはやめよう)」という記述があり、この最適化が複数回試みられ、そのたびに問題が発生していたことが示唆されています。

変更の背景

encoding/json パッケージの Unmarshal 関数は、JSONデータをGoのデータ構造にデコードするための重要な関数です。パフォーマンスは常に重要な考慮事項であり、開発者はこの関数の処理速度を向上させるための最適化を試みていました。

このコミットの背景には、Unmarshal 関数の「2回目の最適化の試み」が、特定の条件下でパニック(プログラムの異常終了)を引き起こすという問題がありました。パニックはGoプログラムにおいて予期せぬエラーであり、通常は回復不可能な状態を示します。ライブラリ関数がパニックを外部に漏洩させることは、そのライブラリを利用するアプリケーションの安定性を著しく損なうため、重大なバグと見なされます。

この最適化は、JSONデータがプリミティブ値(文字列、数値、真偽値、null)である場合に、オブジェクトや配列の複雑なパース処理をスキップすることで、パフォーマンスを向上させようとするものでした。しかし、このショートカットが、特定の不正な入力やエッジケースにおいて、内部的なデータ処理の不整合を引き起こし、結果としてパニックに至ったと考えられます。

コミットメッセージにある Fixes #4784 は、この問題がGoのIssueトラッカーで報告されていたことを示しています。残念ながら、Issue #4784に関する具体的な情報は現在の検索では見つかりませんでしたが、これはGoの古いIssueであるか、非公開のIssueである可能性があります。

前提知識の解説

Go言語の encoding/json パッケージ

encoding/json パッケージは、Go言語でJSON (JavaScript Object Notation) データをエンコード(Goのデータ構造からJSONへ)およびデコード(JSONからGoのデータ構造へ)するための標準ライブラリです。

  • json.Unmarshal(data []byte, v interface{}) error: この関数は、JSON形式のバイトスライス data を受け取り、それをGoのインターフェース v が指すデータ構造にデコードします。v はポインタである必要があります。デコードが成功すると nil を返し、失敗するとエラーを返します。
  • JSONのデータ型: JSONは、オブジェクト({})、配列([])、文字列("")、数値、真偽値(true/false)、null の6つの基本データ型をサポートします。

Go言語の reflect パッケージ

reflect パッケージは、Goプログラムが実行時に自身の構造を検査(リフレクション)したり、変更したりするための機能を提供します。

  • reflect.ValueOf(v interface{}): インターフェース値 v のリフレクト値(reflect.Value)を返します。これにより、実行時に変数の型や値を調べることができます。
  • reflect.Kind(): reflect.Value の基底型(例: reflect.Ptr, reflect.Struct, reflect.Int など)を返します。
  • reflect.Ptr: ポインタ型を示します。
  • reflect.IsNil(): ポインタ、インターフェース、マップ、スライス、関数、チャネルなどの値が nil であるかどうかをチェックします。
  • reflect.Elem(): ポインタが指す要素の reflect.Value を返します。

最適化とパニック

  • 最適化: プログラムの実行速度を向上させたり、リソース使用量を削減したりするための変更です。多くの場合、特定の一般的なケースで処理を高速化するために、より複雑な汎用的な処理をスキップする「ショートカット」を導入します。
  • パニック (Panic): Go言語におけるパニックは、プログラムが回復不可能なエラー状態に陥ったことを示します。これは通常、プログラマの論理的な誤り(例: nilポインタのデリファレンス、配列の範囲外アクセス)によって引き起こされます。パニックが発生すると、現在のゴルーチンは実行を停止し、遅延関数が実行され、最終的にプログラム全体がクラッシュする可能性があります。ライブラリ関数がパニックを外部に漏洩させることは、そのライブラリの利用者に予期せぬクラッシュを引き起こすため、非常に望ましくありません。

技術的詳細

このコミットでロールバックされた最適化は、Unmarshal 関数がJSONデータをパースする際に、入力データがオブジェクト({で始まる)や配列([で始まる)ではない場合に、より軽量な処理パスに分岐させようとするものでした。

具体的には、Unmarshal 関数はまず入力バイトスライス data の先頭から空白文字をスキップし、最初の非空白文字 first を特定します。もし first{ でも [ でもない場合(つまり、JSON文字列が数値、文字列、真偽値、nullなどのプリミティブ値である場合)、Unmarshalreflect パッケージを使って v の型をチェックし、それがポインタであり nil でないことを確認します。その後、d.literalStore(data[i:], rv.Elem(), false) を呼び出して、残りのデータを直接 v が指す要素に格納しようとします。

この最適化の意図は、JSONがオブジェクトや配列でないことが早い段階で判明した場合に、d.init(data)d.unmarshal(v) といったより重い汎用的なパースロジックの呼び出しを避けることでした。これにより、例えば json.Unmarshal([]byte("123"), &myInt) のような単純なケースでパフォーマンスが向上することが期待されました。

しかし、この「ショートカット」ロジックにはバグがあり、特定の入力に対して json パッケージの内部でパニックを引き起こし、それがパッケージの境界を越えて呼び出し元に伝播してしまいました。コミットメッセージは「The second attempt at the Unmarshal optimization allowed panics to get out of the json package.」と述べており、この最適化が以前にも試みられ、問題を起こしていたことを示唆しています。

追加されたテストケース TestUnmarshalJSONLiteralError は、このパニックを引き起こす特定のシナリオを再現するために設計されています。このテストでは、Time3339 というカスタム型を定義し、その UnmarshalJSON メソッド内で time.Parse を使用して日付文字列をパースします。"0000-00-00T00:00:00Z" のような不正な日付文字列を Unmarshal に渡すと、time.Parse がエラーを返すだけでなく、最適化されたパスが予期せぬ動作を引き起こし、パニックに至っていたと考えられます。最適化のロールバックにより、このテストは期待通りにエラーを返し、パニックは発生しなくなります。

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

src/pkg/encoding/json/decode.go

--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -56,8 +56,7 @@ import (
 // an UnmarshalTypeError describing the earliest such error.
 //
 func Unmarshal(data []byte, v interface{}) error {
-
-	// Quick check for well-formedness.
+	// Check for well-formedness.
 	// Avoids filling out half a data structure
 	// before discovering a JSON syntax error.
 	var d decodeState
@@ -66,23 +65,6 @@ func Unmarshal(data []byte, v interface{}) error {
 		return err
 	}
 
-	// skip heavy processing for primitive values
-	var first byte
-	var i int
-	for i, first = range data {
-		if first > ' ' || !isSpace(rune(first)) {
-			break
-		}
-	}
-	if first != '{' && first != '[' {
-		rv := reflect.ValueOf(v)
-		if rv.Kind() != reflect.Ptr || rv.IsNil() {
-			return &InvalidUnmarshalError{reflect.TypeOf(v)}
-		}
-		d.literalStore(data[i:], rv.Elem(), false)
-		return d.savedError
-	}
-
 	d.init(data)
 	return d.unmarshal(v)
 }

src/pkg/encoding/json/decode_test.go

--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -11,6 +11,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 )
 
 type T struct {
@@ -1113,3 +1114,30 @@ func TestUnmarshalUnexported(t *testing.T) {
 	t.Errorf("got %q, want %q", out, want)
 	}
 }
+
+// Time3339 is a time.Time which encodes to and from JSON
+// as an RFC 3339 time in UTC.
+type Time3339 time.Time
+
+func (t *Time3339) UnmarshalJSON(b []byte) error {
+	if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' {
+		return fmt.Errorf("types: failed to unmarshal non-string value %q as an RFC 3339 time")
+	}
+	tm, err := time.Parse(time.RFC3339, string(b[1:len(b)-1]))
+	if err != nil {
+		return err
+	}
+	*t = Time3339(tm)
+	return nil
+}
+
+func TestUnmarshalJSONLiteralError(t *testing.T) {
+	var t3 Time3339
+	err := Unmarshal([]byte(`"0000-00-00T00:00:00Z"`), &t3)
+	if err == nil {
+		t.Fatalf("expected error; got time %v", time.Time(t3))
+	}
+	if !strings.Contains(err.Error(), "range") {
+		t.Errorf("got err = %v; want out of range error", err)
+	}
+}

コアとなるコードの解説

src/pkg/encoding/json/decode.go の変更

  • 削除された最適化ブロック: Unmarshal 関数から、JSON入力の最初の非空白文字が { または [ ではない場合に、プリミティブ値として直接処理しようとする約20行のコードブロックが完全に削除されました。 このブロックは、入力データがオブジェクトや配列でない場合に、reflect パッケージを使用して v の型をチェックし、d.literalStore を呼び出して直接値を格納しようとしていました。 この削除により、すべてのJSON入力は、オブジェクトや配列であるかどうかにかかわらず、d.init(data)d.unmarshal(v) を通じた汎用的なデコードパスで処理されるようになります。これにより、特定のケースでのパフォーマンス上の利点は失われますが、パニックを引き起こす可能性のあるエッジケースが排除され、堅牢性が向上します。

src/pkg/encoding/json/decode_test.go の変更

  • Time3339 型の追加: time.Time をラップし、RFC 3339形式のJSON文字列としてエンコード/デコードするためのカスタム型 Time3339 が追加されました。この型は、json.Unmarshaler インターフェースを満たす UnmarshalJSON メソッドを実装しています。このメソッドは、入力バイトスライスが有効なJSON文字列であることを確認し、time.Parse(time.RFC3339, ...) を使用して日付文字列をパースします。

  • TestUnmarshalJSONLiteralError テスト関数の追加: この新しいテスト関数は、ロールバックされた最適化が引き起こしていたパニックを再現し、修正が正しく行われたことを検証するために追加されました。 テストでは、"0000-00-00T00:00:00Z" という不正な日付文字列を Time3339 型の変数に Unmarshal しようとします。この日付は time.Parse が「out of range」エラーを返すような不正な値です。 最適化が有効だった場合、この不正な入力がパニックを引き起こしていました。最適化がロールバックされた後、このテストはパニックではなく、期待されるエラー(time.Parse からの「out of range」エラー)が返されることを検証します。これにより、Unmarshal 関数が不正な入力に対して適切にエラーを返し、パニックしないことが保証されます。

これらの変更は、パフォーマンスよりも安定性と堅牢性を優先するという明確な決定を示しています。

関連リンク

参考にした情報源リンク

  • この解説は、主に上記のコミットメッセージとコードの差分に基づいて作成されました。
  • Go言語の encoding/json パッケージのドキュメント (Go公式ドキュメント)
  • Go言語の reflect パッケージのドキュメント (Go公式ドキュメント)
  • Go言語におけるパニックとエラーハンドリングに関する一般的な知識
  • RFC 3339 (Date and Time on the Internet: Timestamps) に関する一般的な知識