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

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

このコミットは、Go言語の標準ライブラリ math/big パッケージにおいて、Int 型(任意精度整数)に対するJSONのマーシャリング(Goのデータ構造からJSONへの変換)およびアンマーシャリング(JSONからGoのデータ構造への変換)のサポートを追加するものです。これにより、math/big.Int 型の値をJSONデータとして容易に扱うことができるようになります。

コミット

commit 13a59b8c6d092075c0ff9d6ec3b5d43b6ead7b39
Author: Robert Griesemer <gri@golang.org>
Date:   Tue May 22 17:20:37 2012 -0700

    math/big: implement JSON un/marshaling support for Ints
    
    Also: simplified some existing tests.
    
    No support for Rats for now because the precision-preserving
    default notation (fractions of the form a/b) is not a valid
    JSON value.
    
    Fixes #3657.
    
    R=golang-dev, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6211079

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

https://github.com/golang/go/commit/13a59b8c6d092075c0ff9d6ec3b5d43b6ead7b39

元コミット内容

math/big パッケージの Int 型にJSONのマーシャリングとアンマーシャリングのサポートを実装しました。 既存のテストも一部簡素化しました。 Rat 型(任意精度有理数)については、現時点ではサポートしていません。これは、精度を維持するデフォルトの表記(a/b形式の分数)が有効なJSON値ではないためです。 Issue #3657 を修正します。

変更の背景

この変更の背景には、Go言語の math/big パッケージで提供される任意精度整数型 Int を、Webサービスやデータ交換で広く利用されるJSON形式でシリアライズ・デシリアライズしたいというニーズがありました。Goの encoding/json パッケージは、Goのデータ構造とJSONの間で変換を行うための標準的な方法を提供しますが、カスタム型がJSONとの間で適切に変換されるためには、json.Marshaler および json.Unmarshaler インターフェースを実装する必要があります。

コミットメッセージにある Fixes #3657 は、この機能追加が特定のバグ報告や機能要望に対応するものであることを示しています。Issue #3657 は、math/big.Int 型がJSONにマーシャリングできないという問題提起であったと推測されます。任意精度整数は、通常の intint64 では表現できない非常に大きな数値を扱う場合に不可欠であり、これらの数値をJSONとして送受信する機能は、金融アプリケーション、暗号通貨、科学計算など、高精度な数値計算を必要とする多くのGoアプリケーションにとって重要です。

また、Rat 型(任意精度有理数)についてはJSONサポートが見送られています。これは、Rat 型の標準的な文字列表現が "a/b" のような分数形式であり、これがJSONの有効な数値形式ではないためです。JSONは数値に対して厳格な形式(整数または浮動小数点数)を要求するため、分数形式は直接JSON数値として表現できません。このため、Rat 型のJSONサポートには、別の表現形式(例: 文字列としての "a/b"、または分子と分母を別々のフィールドとして持つオブジェクト)を検討する必要があり、このコミットでは Int 型に焦点を当てています。

前提知識の解説

1. Go言語の math/big パッケージ

math/big パッケージは、Go言語で任意精度の数値演算を行うための型と関数を提供します。これは、標準のGoの組み込み型(int, int64, float64など)では表現できない、非常に大きな整数、有理数、または浮動小数点数を扱う必要がある場合に利用されます。

  • *big.Int: 任意精度の整数型です。メモリが許す限り、どんなに大きな整数でも表現できます。
  • *big.Rat: 任意精度の有理数型です。分子と分母に *big.Int を使用して、正確な分数値を表現します。
  • *big.Float: 任意精度の浮動小数点数型です。

これらの型は、通常の数値型とは異なり、値のコピーではなくポインタとして扱われることが一般的です。

2. JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析・生成しやすいデータ交換フォーマットです。WebアプリケーションやAPIで広く利用されています。JSONのデータ型には、オブジェクト、配列、文字列、数値、真偽値(true/false)、nullがあります。

  • 数値: JSONの数値は、整数または浮動小数点数として表現されます。指数表記も可能です。JSONの仕様では、数値の精度や範囲は特に定められていませんが、一般的にはIEEE 754倍精度浮動小数点数に準拠した実装が多いため、非常に大きな整数や高精度な小数を直接表現するには限界があります。

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

encoding/json パッケージは、Goのデータ構造とJSONの間で変換を行うための機能を提供します。

  • マーシャリング (Marshaling): Goのデータ構造をJSONバイト列に変換するプロセスです。json.Marshal() 関数を使用します。
  • アンマーシャリング (Unmarshaling): JSONバイト列をGoのデータ構造に変換するプロセスです。json.Unmarshal() 関数を使用します。

カスタム型がJSONとの間で適切に変換されるためには、以下のインターフェースを実装することが一般的です。

  • json.Marshaler インターフェース:

    type Marshaler interface {
        MarshalJSON() ([]byte, error)
    }
    

    このインターフェースを実装することで、型自身がJSONへの変換方法を定義できます。MarshalJSON メソッドは、JSON形式のバイトスライスとエラーを返します。

  • json.Unmarshaler インターフェース:

    type Unmarshaler interface {
        UnmarshalJSON([]byte) error
    }
    

    このインターフェースを実装することで、型自身がJSONからの変換方法を定義できます。UnmarshalJSON メソッドは、JSONバイトスライスを受け取り、その内容を自身のフィールドにデコードします。

math/big.Int のような任意精度整数は、標準のJSON数値型では表現しきれない可能性があるため、通常はJSON文字列として表現されます。例えば、123456789012345678901234567890 のような大きな整数は、JSONでは "123456789012345678901234567890" のように文字列として扱われるのが一般的です。このコミットもこのアプローチを採用しています。

4. Go言語の encoding/gob パッケージ

encoding/gob パッケージは、Goのデータ構造をGoプログラム間でエンコード/デコードするための形式を提供します。これはJSONとは異なり、Go固有のバイナリ形式であり、Goプログラム間の通信や永続化に適しています。このコミットでは、既存の gob エンコーディングテストを簡素化し、そのテストケースをJSONエンコーディングテストでも再利用しています。

技術的詳細

このコミットの主要な変更点は、math/big.Int 型に MarshalJSONUnmarshalJSON メソッドを追加し、encoding/json パッケージとの互換性を持たせたことです。

  1. MarshalJSON メソッドの実装 (src/pkg/math/big/int.go):

    • func (x *Int) MarshalJSON() ([]byte, error) として定義されています。
    • このメソッドは、*big.Int の値をJSONバイトスライスに変換します。
    • 実装は非常にシンプルで、x.String() を呼び出して Int の文字列表現を取得し、それを []byte に変換して返します。
    • これにより、*big.Int の値はJSONでは "12345" のような文字列として表現されます。これは、JSONの数値型が任意精度をサポートしないため、大きな整数を正確に表現するための一般的なアプローチです。
  2. UnmarshalJSON メソッドの実装 (src/pkg/math/big/int.go):

    • func (z *Int) UnmarshalJSON(x []byte) error として定義されています。
    • このメソッドは、JSONバイトスライスを *big.Int の値に変換します。
    • 入力された []bytestring(x) で文字列に変換し、z.SetString(string(x), 0) を呼び出して Int の値を設定します。SetString の第2引数 0 は、基数を自動的に検出することを示します(例: "0x" プレフィックスがあれば16進数、そうでなければ10進数)。
    • SetString が失敗した場合(例: 無効な数値文字列が入力された場合)、fmt.Errorf を使用してエラーを返します。
    • TODO(gri): get rid of the []byte/string conversions というコメントがあり、これは将来的に []byte から直接 Int を設定する、より効率的な方法を検討する余地があることを示唆しています。
  3. テストの追加と簡素化 (src/pkg/math/big/int_test.go, src/pkg/math/big/rat_test.go):

    • encoding/json パッケージがインポートされました。
    • 既存の gobEncodingTests 変数が encodingTests にリネームされ、負の数を含むより広範なテストケースが追加されました。これにより、gobjson の両方のエンコーディングテストで同じデータセットを再利用できるようになりました。
    • TestIntGobEncoding および TestRatGobEncoding のテストロジックが簡素化されました。特に、負の数や分数(Ratの場合)を生成するための複雑なループが削除され、encodingTests に直接それらのケースが含まれるようになりました。
    • TestIntJSONEncoding という新しいテスト関数が追加されました。このテストは、encodingTests の各文字列に対して *big.Int を作成し、それをJSONにマーシャリングし、再度アンマーシャリングして、元の値と一致するかどうかを検証します。これにより、JSONのエンコード/デコード機能が正しく動作することが保証されます。

この変更により、math/big.Int 型はGoの encoding/json パッケージの標準的なメカニズムを通じて、JSONデータとしてシームレスに扱えるようになりました。

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

src/pkg/math/big/int.go

@@ -894,3 +894,19 @@ func (z *Int) GobDecode(buf []byte) error {
 	z.abs = z.abs.setBytes(buf[1:])
 	return nil
 }
+
+// MarshalJSON implements the json.Marshaler interface.
+func (x *Int) MarshalJSON() ([]byte, error) {
+	// TODO(gri): get rid of the []byte/string conversions
+	return []byte(x.String()), nil
+}
+
+// UnmarshalJSON implements the json.Unmarshaler interface.
+func (z *Int) UnmarshalJSON(x []byte) error {
+	// TODO(gri): get rid of the []byte/string conversions
+	_, ok := z.SetString(string(x), 0)
+	if !ok {
+		return fmt.Errorf("math/big: cannot unmarshal %s into a *big.Int", x)
+	}
+	return nil
+}

src/pkg/math/big/int_test.go

@@ -8,6 +8,7 @@ import (
 	"bytes"
 	"encoding/gob"
 	"encoding/hex"
+	"encoding/json" // 追加
 	"fmt"
 	"math/rand"
 	"testing"
@@ -1368,8 +1369,12 @@ func TestModInverse(t *testing.T) {
 	}
 }
 
-// used by TestIntGobEncoding and TestRatGobEncoding
-var gobEncodingTests = []string{ // gobEncodingTests から encodingTests にリネーム
+var encodingTests = []string{
+	"-539345864568634858364538753846587364875430589374589", // 負の大きな数
+	"-678645873", // 負の数
+	"-100",
+	"-2",
+	"-1",
 	"0",
 	"1",
 	"2",
@@ -1383,26 +1388,37 @@ func TestIntGobEncoding(t *testing.T) {
 	var medium bytes.Buffer
 	enc := gob.NewEncoder(&medium)
 	dec := gob.NewDecoder(&medium)
-	for i, test := range gobEncodingTests { // gobEncodingTests から encodingTests に変更
-		for j := 0; j < 2; j++ { // 負の数を生成するループを削除
-			medium.Reset() // empty buffer for each test case (in case of failures)
-			stest := test
-			if j != 0 {
-				// negative numbers
-				stest = "-" + test
-			}
-			var tx Int
-			tx.SetString(stest, 10)
-			if err := enc.Encode(&tx); err != nil {
-				t.Errorf("#%d%c: encoding failed: %s", i, 'a'+j, err)
-			}
-			var rx Int
-			if err := dec.Decode(&rx); err != nil {
-				t.Errorf("#%d%c: decoding failed: %s", i, 'a'+j, err)
-			}
-			if rx.Cmp(&tx) != 0 {
-				t.Errorf("#%d%c: transmission failed: got %s want %s", i, 'a'+j, &rx, &tx)
-			}
+	for _, test := range encodingTests { // 簡素化されたループ
+		medium.Reset()
+		var tx Int
+		tx.SetString(test, 10)
+		if err := enc.Encode(&tx); err != nil {
+			t.Errorf("encoding of %s failed: %s", &tx, err)
+		}
+		var rx Int
+		if err := dec.Decode(&rx); err != nil {
+			t.Errorf("decoding of %s failed: %s", &tx, err)
+		}
+		if rx.Cmp(&tx) != 0 {
+			t.Errorf("transmission of %s failed: got %s want %s", &tx, &rx, &tx)
+		}
+	}
+}
+
+// TestIntJSONEncoding 新しいテスト関数
+func TestIntJSONEncoding(t *testing.T) {
+	for _, test := range encodingTests {
+		var tx Int
+		tx.SetString(test, 10)
+		b, err := json.Marshal(&tx)
+		if err != nil {
+			t.Errorf("marshaling of %s failed: %s", &tx, err)
+		}
+		var rx Int
+		if err := json.Unmarshal(b, &rx); err != nil {
+			t.Errorf("unmarshaling of %s failed: %s", &tx, err)
+		}
+		if rx.Cmp(&tx) != 0 {
+			t.Errorf("JSON encoding of %s failed: got %s want %s", &tx, &rx, &tx)
 		}
 	}
 }

src/pkg/math/big/rat_test.go

@@ -387,30 +387,19 @@ func TestRatGobEncoding(t *testing.T) {
 	var medium bytes.Buffer
 	enc := gob.NewEncoder(&medium)
 	dec := gob.NewDecoder(&medium)
-	for i, test := range gobEncodingTests { // gobEncodingTests から encodingTests に変更
-		for j := 0; j < 4; j++ { // 負の数や分数を生成するループを削除
-			medium.Reset() // empty buffer for each test case (in case of failures)
-			stest := test
-			if j&1 != 0 {
-				// negative numbers
-				stest = "-" + test
-			}
-			if j%2 != 0 {
-				// fractions
-				stest = stest + "." + test
-			}
-			var tx Rat
-			tx.SetString(stest)
-			if err := enc.Encode(&tx); err != nil {
-				t.Errorf("#%d%c: encoding failed: %s", i, 'a'+j, err)
-			}
-			var rx Rat
-			if err := dec.Decode(&rx); err != nil {
-				t.Errorf("#%d%c: decoding failed: %s", i, 'a'+j, err)
-			}
-			if rx.Cmp(&tx) != 0 {
-				t.Errorf("#%d%c: transmission failed: got %s want %s", i, 'a'+j, &rx, &tx)
-			}
+	for _, test := range encodingTests { // 簡素化されたループ
+		medium.Reset()
+		var tx Rat
+		tx.SetString(test + ".14159265") // 固定の小数部を追加
+		if err := enc.Encode(&tx); err != nil {
+			t.Errorf("encoding of %s failed: %s", &tx, err)
+		}
+		var rx Rat
+		if err := dec.Decode(&rx); err != nil {
+			t.Errorf("decoding of %s failed: %s", &tx, err)
+		}
+		if rx.Cmp(&tx) != 0 {
+			t.Errorf("transmission of %s failed: got %s want %s", &tx, &rx, &tx)
 		}
 	}
 }

コアとなるコードの解説

src/pkg/math/big/int.go の変更

  • MarshalJSON メソッド: このメソッドは json.Marshaler インターフェースを実装しており、*big.Int 型の値をJSON形式に変換する際の挙動を定義します。 return []byte(x.String()), nil という行がその核心です。これは、*big.IntString() メソッド(数値の10進数文字列表現を返す)を呼び出し、その結果の文字列をバイトスライスに変換して返します。 これにより、例えば big.NewInt(12345) はJSONでは "12345" という文字列として表現されます。これは、JSONの数値型が任意精度をサポートしないため、大きな整数を正確に表現するための標準的な方法です。 TODO(gri): get rid of the []byte/string conversions というコメントは、文字列への変換を介さずに直接バイトスライスを生成する、より効率的な方法がある可能性を示唆しています。

  • UnmarshalJSON メソッド: このメソッドは json.Unmarshaler インターフェースを実装しており、JSONバイトスライスを *big.Int 型の値に変換する際の挙動を定義します。 _, ok := z.SetString(string(x), 0) という行がその核心です。これは、入力されたJSONバイトスライス x をGoの文字列に変換し、その文字列を *big.IntSetString メソッドに渡して値を設定します。SetString の第2引数 0 は、入力文字列の基数(10進数、16進数など)を自動的に検出するように指示します。 if !ok { return fmt.Errorf(...) } の部分は、SetString が失敗した場合(例えば、JSONが有効な数値文字列を含んでいない場合)に適切なエラーを返すためのエラーハンドリングです。 こちらも TODO(gri): get rid of the []byte/string conversions というコメントがあり、効率化の余地が示されています。

src/pkg/math/big/int_test.go および src/pkg/math/big/rat_test.go の変更

  • encoding/json のインポート: int_test.goimport "encoding/json" が追加され、JSON関連のテストが可能になりました。
  • encodingTests 変数の導入: 以前は gobEncodingTests という名前だった変数が encodingTests にリネームされ、負の数や非常に大きな負の数を含む、より包括的なテストケースが追加されました。これにより、gobjson の両方のエンコーディングテストで同じデータセットを共有できるようになり、テストの保守性が向上しました。
  • 既存テストの簡素化: TestIntGobEncodingTestRatGobEncoding の内部ロジックが簡素化されました。以前はテストケース内で負の数や分数(Ratの場合)を動的に生成していましたが、encodingTests にそれらのケースが直接含まれるようになったため、テストコードがより読みやすく、簡潔になりました。
  • TestIntJSONEncoding の追加: この新しいテスト関数は、*big.Int のJSONマーシャリングとアンマーシャリングが正しく機能するかを検証します。
    • json.Marshal(&tx)*big.Int をJSONバイトスライスに変換します。
    • json.Unmarshal(b, &rx) でそのJSONバイトスライスを新しい *big.Int に変換します。
    • rx.Cmp(&tx) != 0 で、元の値 tx とアンマーシャリングされた値 rx が等しいかどうかを比較します。Cmp メソッドは、2つの Int の比較結果を返します(0であれば等しい)。
    • エラーが発生した場合や値が一致しない場合に、詳細なエラーメッセージを出力します。

これらの変更により、math/big.Int はGoの標準的なJSONエンコーディング/デコーディングメカニズムに完全に統合され、開発者は *big.Int を他のGoのデータ構造と同様にJSONとして扱うことができるようになりました。

関連リンク

  • Go issue #3657: https://code.google.com/p/go/issues/detail?id=3657 (古いGoogle Codeのリンクですが、コミットメッセージに記載されています)
  • Gerrit Change-Id: https://golang.org/cl/6211079 (GoのコードレビューシステムGerritの変更リスト)

参考にした情報源リンク