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

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

このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおいて、特殊な浮動小数点値(NaN: Not a Number, Infinity: 無限大)がJSONにマーシャリングされるのを防ぐための変更を導入しています。これにより、JSON仕様に準拠しない値が生成されることを避け、より堅牢なJSONエンコーディングを実現します。

コミット

commit c20c09251c37c60356e8457a3c7cb632c30b69b1
Author: Evan Shaw <chickencha@gmail.com>
Date:   Tue Jan 3 12:30:18 2012 +1100

    encoding/json: don't marshal special float values

    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/5500084

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

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

元コミット内容

encoding/json: don't marshal special float values

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5500084

変更の背景

JSON (JavaScript Object Notation) は、データ交換のための軽量なフォーマットであり、その仕様は厳密に定義されています。しかし、IEEE 754浮動小数点標準で定義されている NaN (Not a Number) や Infinity (無限大) といった特殊な浮動小数点値は、標準のJSON仕様では直接的な表現方法がありません。

過去には、JavaScriptの JSON.stringify() 関数がこれらの値を null に変換したり、非標準の文字列(例: "NaN", "Infinity")として出力したりする実装が存在しました。しかし、これはJSONの相互運用性や厳密なデータ型定義に問題を引き起こす可能性がありました。特に、JSONパーサーがこれらの非標準の文字列を数値として解釈できない場合、データ損失やエラーの原因となります。

Go言語の encoding/json パッケージは、JSON仕様に厳密に準拠することを目指しています。このコミットの背景には、特殊な浮動小数点値がJSONとして出力された場合に、そのJSONが標準仕様に違反し、他のシステムでのパースに問題が生じることを防ぐという目的があります。これにより、GoのJSONエンコーダが生成するJSONの信頼性と互換性が向上します。

前提知識の解説

JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。JavaScriptのオブジェクトリテラル表記に由来しますが、言語に依存しないデータ形式として広く利用されています。JSONのデータ型には、文字列、数値、真偽値、null、オブジェクト、配列があります。

IEEE 754 浮動小数点標準

ほとんどの現代のコンピュータシステムで浮動小数点数を表現するために使用される国際標準です。この標準では、通常の数値の他に、以下の特殊な値を定義しています。

  • NaN (Not a Number): 不定形な結果(例: 0/0、無限大/無限大)を表すために使用されます。
  • Infinity (無限大): 数値が表現できる最大値を超える場合(例: 1/0)に、正の無限大 (+Inf) または負の無限大 (-Inf) として使用されます。

JSONと特殊浮動小数点値の互換性問題

標準のJSON仕様(RFC 8259など)では、NaNInfinity を直接表現するための構文がありません。JSONの数値は、10進数表記の有限な数値のみを許容します。そのため、これらの特殊な浮動小数点値をJSONに含めようとすると、以下のいずれかの問題が発生します。

  1. 非標準の文字列として出力: "NaN""Infinity" のような文字列として出力される場合、JSONパーサーがこれを数値として認識できず、文字列として扱ってしまう可能性があります。
  2. nullへの変換: 一部の実装では null に変換されますが、これは元の情報の損失を意味します。
  3. エラー: 厳密なJSONエンコーダは、これらの値を検出した際にエラーを発生させることがあります。

このコミットは、3番目の「エラー」を発生させるアプローチを採用することで、JSON仕様への厳密な準拠と、意図しないデータ表現を防ぐことを目的としています。

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

Go言語の標準ライブラリに含まれるパッケージで、Goのデータ構造とJSONデータの間でマーシャリング(Goの構造体をJSONに変換)およびアンマーシャリング(JSONをGoの構造体に変換)を行う機能を提供します。json.Marshal 関数はGoの値をJSONバイトスライスに変換し、json.Unmarshal 関数はその逆を行います。

技術的詳細

このコミットの主要な変更点は、encoding/json パッケージのエンコーダが浮動小数点値を処理する際に、math.NaN()math.Inf(-1) (負の無限大)、math.Inf(1) (正の無限大) といった特殊な値を検出した場合に、エラーを発生させるようにしたことです。

具体的には、以下の変更が行われました。

  1. math パッケージのインポート: 浮動小数点値のチェックのために math.IsInf および math.IsNaN 関数を使用するため、src/pkg/encoding/json/encode.go および src/pkg/encoding/json/encode_test.goimport "math" が追加されました。
  2. UnsupportedValueError 型の導入: 新しいエラー型 UnsupportedValueError が定義されました。このエラーは、サポートされていない値(この場合は特殊な浮動小数点値)がマーシャリングされようとしたときに返されます。これにより、どのような値が問題を引き起こしたのかを具体的に示すことができます。
    type UnsupportedValueError struct {
        Value reflect.Value
        Str   string
    }
    
    func (e *UnsupportedValueError) Error() string {
        return "json: unsupported value: " + e.Str
    }
    
  3. 浮動小数点値のチェックとエラー発生: encodeState.reflectValueQuoted メソッド内で、reflect.Float32 および reflect.Float64 型の値を処理するロジックが変更されました。
    • 浮動小数点値 f を取得した後、math.IsInf(f, 0) または math.IsNaN(f) を使用して、その値が無限大またはNaNであるかをチェックします。
    • もしこれらの特殊な値であった場合、e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())}) を呼び出して、UnsupportedValueError を発生させます。これにより、json.Marshal 関数はエラーを返して処理を中断します。
    • 通常の有限な浮動小数点値は、これまで通り strconv.AppendFloat を使用して文字列に変換されます。
  4. テストケースの追加: src/pkg/encoding/json/encode_test.goTestUnsupportedValues という新しいテスト関数が追加されました。このテストは、math.NaN(), math.Inf(-1), math.Inf(1) を含むスライス unsupportedValues を定義し、これらの値を json.Marshal でマーシャリングしようとします。期待される動作は、UnsupportedValueError が返されることです。これにより、変更が正しく機能していることが保証されます。

この変更により、Goの encoding/json パッケージは、JSON仕様に準拠しない特殊な浮動小数点値のエンコーディングを明示的に拒否するようになります。これは、生成されるJSONの品質と互換性を高めるための重要なステップです。

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

src/pkg/encoding/json/encode.go

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -12,6 +12,7 @@ package json
 import (
 	"bytes"
 	"encoding/base64"
+	"math"
 	"reflect"
 	"runtime"
 	"sort"
@@ -170,6 +171,15 @@ func (e *UnsupportedTypeError) Error() string {
 	return "json: unsupported type: " + e.Type.String()
 }
 
+type UnsupportedValueError struct {
+	Value reflect.Value
+	Str   string
+}
+
+func (e *UnsupportedValueError) Error() string {
+	return "json: unsupported value: " + e.Str
+}
+
 type InvalidUTF8Error struct {
 	S string
 }
@@ -290,7 +300,11 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) {
 		e.Write(b)
 	}
 	case reflect.Float32, reflect.Float64:
-		b := strconv.AppendFloat(e.scratch[:0], v.Float(), 'g', -1, v.Type().Bits())
+		f := v.Float()
+		if math.IsInf(f, 0) || math.IsNaN(f) {
+			e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())})
+		}
+		b := strconv.AppendFloat(e.scratch[:0], f, 'g', -1, v.Type().Bits())
 		if quoted {
 			writeString(e, string(b))
 		} else {

src/pkg/encoding/json/encode_test.go

--- a/src/pkg/encoding/json/encode_test.go
+++ b/src/pkg/encoding/json/encode_test.go
@@ -6,6 +6,7 @@ package json
 
 import (
 	"bytes"
+	"math"
 	"reflect"
 	"testing"
 )
@@ -107,3 +108,21 @@ func TestEncodeRenamedByteSlice(t *testing.T) {
 		t.Errorf(" got %s want %s", result, expect)
 	}
 }
+
+var unsupportedValues = []interface{}{
+	math.NaN(),
+	math.Inf(-1),
+	math.Inf(1),
+}
+
+func TestUnsupportedValues(t *testing.T) {
+	for _, v := range unsupportedValues {
+		if _, err := Marshal(v); err != nil {
+			if _, ok := err.(*UnsupportedValueError); !ok {
+				t.Errorf("for %v, got %T want UnsupportedValueError", v, err)
+			}
+		} else {
+			t.Errorf("for %v, expected error", v)
+		}
+	}
+}

コアとなるコードの解説

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

  1. import "math" の追加: math パッケージは、浮動小数点数に関する数学関数を提供します。特に math.IsInfmath.IsNaN は、それぞれ値が無限大であるか、NaNであるかを判定するために使用されます。これらの関数は、特殊な浮動小数点値を検出するために不可欠です。

  2. UnsupportedValueError 構造体の定義: この新しいエラー型は、encoding/json パッケージがマーシャリングできない値を検出した際に、より具体的なエラー情報を提供するために導入されました。Value フィールドは元の reflect.Value を保持し、Str フィールドは問題となった値の文字列表現を保持します。これにより、デバッグ時にどの値がエラーの原因となったかを特定しやすくなります。

  3. reflect.Float32, reflect.Float64 ケースの変更: encodeState.reflectValueQuoted メソッドは、Goの値をJSONにエンコードする際の中心的なロジックの一部です。このメソッドが float32 または float64 型の値を処理する際に、以下の重要な変更が加えられました。

    • f := v.Float(): reflect.Value から実際の浮動小数点値を取得します。
    • if math.IsInf(f, 0) || math.IsNaN(f): ここがこのコミットの核心部分です。取得した浮動小数点値 f が、math.IsInf (無限大であるか) または math.IsNaN (NaNであるか) のいずれかに該当するかをチェックします。math.IsInf(f, 0) の第2引数 0 は、正負どちらの無限大もチェックすることを意味します。
    • e.error(&UnsupportedValueError{v, strconv.FormatFloat(f, 'g', -1, v.Type().Bits())}): もし値が無限大またはNaNであった場合、e.error メソッドを呼び出して、UnsupportedValueError を発生させます。strconv.FormatFloat は、エラーメッセージに含めるために、問題の浮動小数点値を文字列に変換します。このエラーの発生により、json.Marshal はエラーを返して処理を中断し、不正なJSONが生成されるのを防ぎます。
    • この変更により、GoのJSONエンコーダは、JSON仕様に準拠しない浮動小数点値の出力を明示的に拒否するようになります。

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

  1. import "math" の追加: テストファイルでも math.NaNmath.Inf を使用して特殊な浮動小数点値を生成するため、math パッケージがインポートされています。

  2. unsupportedValues 変数の定義: unsupportedValues は、テスト対象となる特殊な浮動小数点値(NaN、負の無限大、正の無限大)を含む interface{} 型のスライスです。これにより、これらの値に対するテストを簡潔に記述できます。

  3. TestUnsupportedValues 関数の追加: このテスト関数は、unsupportedValues スライス内の各値に対して以下の検証を行います。

    • if _, err := Marshal(v); err != nil: 各値を json.Marshal でマーシャリングしようとします。この変更の目的はエラーを発生させることなので、errnil でないことを期待します。
    • if _, ok := err.(*UnsupportedValueError); !ok: 返されたエラーが UnsupportedValueError 型であることを確認します。これにより、正しい種類のエラーが返されていることを保証します。
    • t.Errorf(...): 期待されるエラーが返されなかった場合、またはエラーが全く返されなかった場合にテストを失敗させます。

これらのテストケースは、encode.go で行われた変更が意図通りに機能し、特殊な浮動小数点値がJSONにマーシャリングされようとしたときに正しくエラーを発生させることを保証します。

関連リンク

  • Go CL 5500084: https://golang.org/cl/5500084 (このコミットに対応するGoのコードレビューシステム上のチェンジリスト)

参考にした情報源リンク