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

[インデックス 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の数値を文字列として読み込み、必要に応じて float64int64 に変換するか、あるいは元の文字列形式をそのまま利用するかを選択できるようになります。これは、特に以下のようなシナリオで重要です。

  • 金融アプリケーション: 厳密な精度が求められる通貨や取引量。
  • 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 は結果の整数のビット幅を指定します。

技術的詳細

このコミットの主要な変更点は以下の通りです。

  1. Number 型の導入: src/pkg/encoding/json/decode.gotype Number string が追加されました。これは、JSONの数値リテラルを文字列として保持するための型です。

  2. Number 型のメソッド: Number 型には以下のメソッドが追加されました。

    • String() string: Number の元の文字列リテラルを返します。
    • Float64() (float64, error): Numberfloat64 に変換します。内部で strconv.ParseFloat を使用します。
    • Int64() (int64, error): Numberint64 に変換します。内部で strconv.ParseInt を使用します。
  3. decodeState 構造体への useNumber フィールドの追加: src/pkg/encoding/json/decode.godecodeState 構造体に useNumber bool フィールドが追加されました。このフラグが true の場合、JSONの数値は float64 ではなく Number 型としてデコードされます。

  4. Decoder.UseNumber() メソッドの追加: src/pkg/encoding/json/stream.gofunc (dec *Decoder) UseNumber() メソッドが追加されました。このメソッドを呼び出すことで、Decoder が数値を Number 型としてデコードするように設定できます。

  5. convertNumber 関数の導入: src/pkg/encoding/json/decode.goconvertNumber(s string) (interface{}, error) 関数が追加されました。この関数は decodeState.useNumber の設定に基づいて、与えられた数値文字列 sNumber 型または float64 型に変換して返します。

  6. デコードロジックの変更:

    • decodeState.literalStore および decodeState.literalInterface 関数内で、数値のデコード処理が strconv.ParseFloat から新しく導入された d.convertNumber を使用するように変更されました。これにより、useNumber フラグの状態に応じて float64 または Number が返されるようになります。
    • literalStore 関数では、reflect.Value の型が Number 型(numberType)である場合に、直接文字列をセットするロジックが追加されました。
  7. エンコードロジックの変更: src/pkg/encoding/json/encode.goencodeState.reflectValueQuoted 関数内で、reflect.String 型の処理に Number 型の特別なハンドリングが追加されました。これにより、Number 型の値は、その文字列リテラルがそのままJSONの数値としてエンコードされるようになります。空の Number の場合は "0" としてエンコードされます。

  8. テストケースの追加: 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 構造体、ifaceNumAsFloat64ifaceNumAsNumber の追加。
  • L56: unmarshalTest 構造体に useNumber bool フィールドを追加。
  • L68-71, L80-81: unmarshalTestsNumber 型および useNumber を使用したテストケースを追加。
  • L145-150: TestMarshalNumberZeroVal テストの追加。
  • L161-164, L174-177: TestUnmarshal 関数内で Decoder.UseNumber() を呼び出すロジックを追加。
  • L208-240: numberTestsTestNumberAccessors テストの追加。

コアとなるコードの解説

Number 型とそのメソッド

Number 型は単なる string のエイリアスですが、JSONの数値リテラルをその文字列形式で保持するというセマンティクスを持ちます。追加された Float64()Int64() メソッドは、必要に応じてこの文字列を数値型に変換するための便利なユーティリティを提供します。これにより、元の精度を保持しつつ、数値としての操作も可能になります。エラーハンドリングも含まれており、変換に失敗した場合にはエラーが返されます。

decodeState.useNumberDecoder.UseNumber()

decodeState はJSONデコード処理の状態を管理する内部構造体です。useNumber フィールドは、デコード時に数値を float64 として扱うか、Number 型として扱うかを制御するフラグです。 Decoder.UseNumber() メソッドは、ユーザーがこの useNumber フラグを true に設定するための公開APIです。これにより、json.NewDecoder を使用してストリームからJSONをデコードする際に、数値の扱いをカスタマイズできるようになります。

convertNumber 関数

この関数は、デコード中に数値リテラル(文字列として取得される)を適切なGoの型に変換する中心的なロジックをカプセル化しています。useNumbertrue であれば Number(s) を返し、そうでなければ strconv.ParseFloat を使って float64 に変換します。これにより、デコード処理の複数の場所で同じロジックを再利用し、コードの重複を避けています。

デコードロジックの変更 (literalStore, literalInterface)

これらの関数は、JSONのプリミティブ値(文字列、数値、真偽値、null)をGoの対応する型にデコードする役割を担っています。変更点では、数値のデコード部分が d.convertNumber を呼び出すように統一されました。これにより、useNumber の設定がデコード結果に反映されるようになります。 また、reflect.String 型のデコード処理において、ターゲットの型が Number 型である場合に、直接文字列をセットする特殊なケースが追加されました。これは、Numberstring のエイリアスであるため、string としてデコードされる可能性があるためです。

エンコードロジックの変更 (reflectValueQuoted)

この関数はGoの値をJSONにエンコードする役割を担っています。Number 型のエンコード処理が追加されたことで、Number 型の値は、その内部の文字列がそのままJSONの数値リテラルとして出力されるようになります。これにより、デコード時に保持された精度と書式が、再エンコード時にも維持されることが保証されます。空の Number が "0" としてエンコードされるのは、JSONの数値として有効な表現を提供するためです。

関連リンク

参考にした情報源リンク