[インデックス 13400] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージに Number 型を追加するものです。これにより、JSONの数値リテラルを float64 としてパースする代わりに、元の文字列形式(精度と書式を保持したまま)で扱うことが可能になります。これは、特に金融データやIDなど、数値の精度が重要となる場面で、浮動小数点数変換による誤差を避けたい場合に有用です。
コミット
commit b7bb1e32d84f45794b2106daa5c908bcb390461e
Author: Jonathan Gold <jgold.bg@gmail.com>
Date: Mon Jun 25 17:36:09 2012 -0400
encoding/json: add Number type
Number represents the actual JSON text,
preserving the precision and
formatting of the original input.
R=rsc, iant
CC=golang-dev
https://golang.org/cl/6202068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b7bb1e32d84f45794b2106daa5c908bcb390461e
元コミット内容
このコミットは、Goの encoding/json パッケージに Number 型を導入することを目的としています。この新しい型は、JSONドキュメント内の数値を、float64 のような浮動小数点型に変換するのではなく、元のテキスト表現のまま保持します。これにより、数値の精度や書式(例: "1.0" と "1"、"1e+10" と "10000000000" の違い)が維持されます。
変更の背景
JSON (JavaScript Object Notation) は、データ交換のための軽量なデータ形式であり、数値型をサポートしています。しかし、多くのプログラミング言語では、JSONの数値をネイティブの浮動小数点型(Goでは float64)にマッピングします。この変換は、特に大きな整数や小数点以下の桁数が多い数値の場合、浮動小数点数の精度限界により、元の数値が持つ精度や書式が失われる可能性があります。
例えば、JavaScriptの Number 型はIEEE 754倍精度浮動小数点数で表現されるため、9007199254740991 (2^53 - 1) を超える整数や、非常に小さい/大きい浮動小数点数は正確に表現できません。Goの float64 も同様の制約を持ちます。
この問題に対処するため、このコミットでは Number 型を導入しました。これにより、開発者はJSONの数値を文字列として読み込み、必要に応じて float64 や int64 に変換するか、あるいは元の文字列形式をそのまま利用するかを選択できるようになります。これは、特に以下のようなシナリオで重要です。
- 金融アプリケーション: 厳密な精度が求められる通貨や取引量。
- IDやハッシュ値: 非常に大きな整数値で、浮動小数点数に変換すると精度が失われる可能性があるもの。
- データの一貫性: JSONドキュメントの元の書式を保持する必要がある場合。
前提知識の解説
JSONの数値表現
JSONでは、数値は整数または浮動小数点数として表現されます。JSONの仕様では、数値の精度や範囲については特に規定されていませんが、通常はIEEE 754倍精度浮動小数点数にマッピングされることが多いです。
Go言語の数値型
float64: Goにおける倍精度浮動小数点数型です。JSONの数値をデフォルトでこの型にデコードします。int64: Goにおける64ビット符号付き整数型です。
encoding/json パッケージのデフォルトの挙動
Goの encoding/json パッケージは、デフォルトではJSONの数値を float64 型にデコードします。例えば、json.Unmarshal を使用して interface{} にデコードした場合、数値は float64 として扱われます。
package main
import (
"encoding/json"
"fmt"
)
func main() {
data := []byte(`{"value": 12345678901234567890.12345}`)
var result map[string]interface{}
json.Unmarshal(data, &result)
fmt.Printf("Type: %T, Value: %v\n", result["value"], result["value"])
// 出力: Type: float64, Value: 1.2345678901234568e+19
// 精度が失われていることがわかる
}
strconv パッケージ
strconv パッケージは、文字列と基本的なデータ型(数値、真偽値など)との間の変換を提供します。
strconv.ParseFloat(s string, bitSize int): 文字列sを浮動小数点数にパースします。bitSizeは結果の浮動小数点数のビット幅(32または64)を指定します。strconv.ParseInt(s string, base int, bitSize int): 文字列sを整数にパースします。baseは基数(例: 10進数なら10)、bitSizeは結果の整数のビット幅を指定します。
技術的詳細
このコミットの主要な変更点は以下の通りです。
-
Number型の導入:src/pkg/encoding/json/decode.goにtype Number stringが追加されました。これは、JSONの数値リテラルを文字列として保持するための型です。 -
Number型のメソッド:Number型には以下のメソッドが追加されました。String() string:Numberの元の文字列リテラルを返します。Float64() (float64, error):Numberをfloat64に変換します。内部でstrconv.ParseFloatを使用します。Int64() (int64, error):Numberをint64に変換します。内部でstrconv.ParseIntを使用します。
-
decodeState構造体へのuseNumberフィールドの追加:src/pkg/encoding/json/decode.goのdecodeState構造体にuseNumber boolフィールドが追加されました。このフラグがtrueの場合、JSONの数値はfloat64ではなくNumber型としてデコードされます。 -
Decoder.UseNumber()メソッドの追加:src/pkg/encoding/json/stream.goにfunc (dec *Decoder) UseNumber()メソッドが追加されました。このメソッドを呼び出すことで、Decoderが数値をNumber型としてデコードするように設定できます。 -
convertNumber関数の導入:src/pkg/encoding/json/decode.goにconvertNumber(s string) (interface{}, error)関数が追加されました。この関数はdecodeState.useNumberの設定に基づいて、与えられた数値文字列sをNumber型またはfloat64型に変換して返します。 -
デコードロジックの変更:
decodeState.literalStoreおよびdecodeState.literalInterface関数内で、数値のデコード処理がstrconv.ParseFloatから新しく導入されたd.convertNumberを使用するように変更されました。これにより、useNumberフラグの状態に応じてfloat64またはNumberが返されるようになります。literalStore関数では、reflect.Valueの型がNumber型(numberType)である場合に、直接文字列をセットするロジックが追加されました。
-
エンコードロジックの変更:
src/pkg/encoding/json/encode.goのencodeState.reflectValueQuoted関数内で、reflect.String型の処理にNumber型の特別なハンドリングが追加されました。これにより、Number型の値は、その文字列リテラルがそのままJSONの数値としてエンコードされるようになります。空のNumberの場合は "0" としてエンコードされます。 -
テストケースの追加:
src/pkg/encoding/json/decode_test.goに、Number型のデコード、エンコード、およびアクセサメソッドの動作を検証するための新しいテストケースが多数追加されました。特に、useNumberフラグの有無によるデコード結果の違いや、Number型のInt64()およびFloat64()メソッドの挙動が詳細にテストされています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
- L139-147:
Number型の定義と、String(),Float64(),Int64()メソッドの追加。// A Number represents a JSON number literal. type Number string // String returns the literal text of the number. func (n Number) String() string { return string(n) } // Float64 returns the number as a float64. func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64) } // Int64 returns the number as an int64. func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64) } - L163:
decodeState構造体にuseNumber boolフィールドを追加。useNumber bool - L596-605:
convertNumber関数の追加。// convertNumber converts the number literal s to a float64 or a Number // depending on the setting of d.useNumber. func (d *decodeState) convertNumber(s string) (interface{}, error) { if d.useNumber { return Number(s), nil } f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0)} } return f, nil } - L607:
numberType変数の追加。var numberType = reflect.TypeOf(Number("")) - L666-670:
literalStore関数内でNumber型へのデコードをサポート。case reflect.String: if v.Type() == numberType { v.SetString(s) break } - L674-678, L828-832:
literalStoreおよびliteralInterface関数内でd.convertNumberを使用するように変更。// literalStore - n, err := strconv.ParseFloat(s, 64) + n, err := d.convertNumber(s) if err != nil { - d.saveError(&UnmarshalTypeError{"number " + s, v.Type()}) + d.saveError(err) break } // literalInterface - n, err := strconv.ParseFloat(string(item), 64) + n, err := d.convertNumber(string(item)) if err != nil { - d.saveError(&UnmarshalTypeError{"number " + string(item), reflect.TypeOf(0.0)}) + d.saveError(err) }
src/pkg/encoding/json/encode.go
- L39: コメントの更新。
// Floating point, integer, and Number values encode as JSON numbers. - L314-320:
reflectValueQuoted関数内でNumber型のエンコードをサポート。case reflect.String: if v.Type() == numberType { numStr := v.String() if numStr == "" { numStr = "0" // Number's zero-val } e.WriteString(numStr) break }
src/pkg/encoding/json/stream.go
- L29-30:
Decoder.UseNumber()メソッドの追加。// UseNumber causes the Decoder to unmarshal a number into an interface{} as a // Number instead of as a float64. func (dec *Decoder) UseNumber() { dec.d.useNumber = true }
src/pkg/encoding/json/decode_test.go
- L24-40:
V構造体、ifaceNumAsFloat64、ifaceNumAsNumberの追加。 - L56:
unmarshalTest構造体にuseNumber boolフィールドを追加。 - L68-71, L80-81:
unmarshalTestsにNumber型およびuseNumberを使用したテストケースを追加。 - L145-150:
TestMarshalNumberZeroValテストの追加。 - L161-164, L174-177:
TestUnmarshal関数内でDecoder.UseNumber()を呼び出すロジックを追加。 - L208-240:
numberTestsとTestNumberAccessorsテストの追加。
コアとなるコードの解説
Number 型とそのメソッド
Number 型は単なる string のエイリアスですが、JSONの数値リテラルをその文字列形式で保持するというセマンティクスを持ちます。追加された Float64() と Int64() メソッドは、必要に応じてこの文字列を数値型に変換するための便利なユーティリティを提供します。これにより、元の精度を保持しつつ、数値としての操作も可能になります。エラーハンドリングも含まれており、変換に失敗した場合にはエラーが返されます。
decodeState.useNumber と Decoder.UseNumber()
decodeState はJSONデコード処理の状態を管理する内部構造体です。useNumber フィールドは、デコード時に数値を float64 として扱うか、Number 型として扱うかを制御するフラグです。
Decoder.UseNumber() メソッドは、ユーザーがこの useNumber フラグを true に設定するための公開APIです。これにより、json.NewDecoder を使用してストリームからJSONをデコードする際に、数値の扱いをカスタマイズできるようになります。
convertNumber 関数
この関数は、デコード中に数値リテラル(文字列として取得される)を適切なGoの型に変換する中心的なロジックをカプセル化しています。useNumber が true であれば Number(s) を返し、そうでなければ strconv.ParseFloat を使って float64 に変換します。これにより、デコード処理の複数の場所で同じロジックを再利用し、コードの重複を避けています。
デコードロジックの変更 (literalStore, literalInterface)
これらの関数は、JSONのプリミティブ値(文字列、数値、真偽値、null)をGoの対応する型にデコードする役割を担っています。変更点では、数値のデコード部分が d.convertNumber を呼び出すように統一されました。これにより、useNumber の設定がデコード結果に反映されるようになります。
また、reflect.String 型のデコード処理において、ターゲットの型が Number 型である場合に、直接文字列をセットする特殊なケースが追加されました。これは、Number が string のエイリアスであるため、string としてデコードされる可能性があるためです。
エンコードロジックの変更 (reflectValueQuoted)
この関数はGoの値をJSONにエンコードする役割を担っています。Number 型のエンコード処理が追加されたことで、Number 型の値は、その内部の文字列がそのままJSONの数値リテラルとして出力されるようになります。これにより、デコード時に保持された精度と書式が、再エンコード時にも維持されることが保証されます。空の Number が "0" としてエンコードされるのは、JSONの数値として有効な表現を提供するためです。
関連リンク
- Go CL (Code Review) 6202068: https://golang.org/cl/6202068
参考にした情報源リンク
- Go言語
encoding/jsonパッケージ公式ドキュメント: https://pkg.go.dev/encoding/json - Go言語
strconvパッケージ公式ドキュメント: https://pkg.go.dev/strconv - JSON (JavaScript Object Notation) 公式サイト: https://www.json.org/json-en.html
- IEEE 754 浮動小数点数標準 (Wikipedia): https://ja.wikipedia.org/wiki/IEEE_754
- Go言語におけるJSONの数値の扱いに関する議論 (Stack Overflowなど): https://stackoverflow.com/questions/tagged/go+json+number (一般的な情報源として)
- Go言語の
encoding/jsonパッケージのソースコード (GitHub): https://github.com/golang/go/tree/master/src/encoding/json (変更点の詳細な確認のため)