Go の高精度計算:big.NewFloat() の詳細と活用事例

2025-06-01

big.NewFloat() は、Go の math/big パッケージで提供されている関数の一つです。この関数は、任意の精度を持つ浮動小数点数を新しく作成するために使用されます。

もう少し詳しく見ていきましょう。

  • NewFloat() 関数
    この関数は、新しい big.Float 型の値を生成します。NewFloat() に引数を渡すことで、初期値を設定できます。

    • 引数なし
      big.NewFloat() のように引数を何も渡さずに呼び出すと、値がゼロの big.Float が作成されます。

    • float64 型の引数
      big.NewFloat(3.14) のように float64 型の値を引数に渡すと、その値で初期化された big.Float が作成されます。ただし、float64 自体の精度には限界があるため、完全に精度を保てない場合があることに注意が必要です。

    • string 型の引数
      big.NewFloat().SetString("1.234567890123456789") のように、SetString() メソッドと組み合わせて文字列で初期値を設定することもできます。この方法を使うと、文字列で指定した通りの精度で big.Float を初期化できます。

  • big パッケージ
    Go の標準ライブラリにある math/big パッケージは、標準の float64 型や int 型よりも大きな範囲や精度を扱うための型を提供しています。big.Float 型はその一つで、非常に大きな数や非常に小さな数を、誤差を少なく正確に表現できます。

big.Float の主な特徴と利用場面

  • 科学技術計算
    非常に大きな数や小さな数を扱う科学技術計算でも役立ちます。
  • 金融計算
    わずかな誤差も許容されない金融計算などで利用されます。
  • 任意精度
    必要に応じて精度を設定できます(ただし、計算コストは上がります)。
  • 高精度
    標準の float64 型よりも遥かに高い精度で数値を扱うことができます。

簡単な例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 引数なしで新しい big.Float を作成(初期値は 0)
	f1 := big.NewFloat(0)
	fmt.Println("f1:", f1) // 出力: f1: 0

	// float64 型の値で初期化
	f2 := big.NewFloat(2.71828)
	fmt.Println("f2:", f2) // 出力: f2: 2.71828

	// 文字列で初期化(より高い精度で設定可能)
	f3 := new(big.Float)
	f3.SetString("3.14159265358979323846")
	fmt.Println("f3:", f3) // 出力: f3: 3.14159265358979323846

	// 計算例
	sum := new(big.Float).Add(f2, f3)
	fmt.Println("f2 + f3 =", sum) // 出力: f2 + f3 = 5.85987265358979323846
}


SetString() メソッドでのエラー

big.NewFloat() で作成した big.Float に文字列から値を設定する際に、SetString() メソッドがよく使われます。このメソッドで発生する可能性のあるエラーは以下の通りです。

  • オーバーフロー/アンダーフロー
    文字列で表現された数値が、big.Float が扱える範囲を超えている場合に、精度が失われる可能性があります(エラーとして明示的に返されない場合もありますが、意図しない結果になることがあります)。

  • 不正な文字列フォーマット
    SetString() に渡す文字列が、浮動小数点数として正しく解釈できない形式の場合にエラーが発生します。例えば、数字、小数点以外の文字が含まれている場合などです。

    f := new(big.Float)
    _, ok := f.SetString("abc") // エラー: "abc" は数値として解釈できない
    if !ok {
        fmt.Println("エラー: 不正な文字列フォーマット")
    }
    

トラブルシューティング

  • 非常に大きな数や小さな数を扱う場合は、big.Float の精度設定(後述)が適切かどうかを確認してください。
  • 入力元のデータを検証し、数値として正しい形式であることを確認してください。
  • SetString() の戻り値(2つ目の bool 型の値)を確認し、false の場合はエラーが発生しています。エラー処理を行い、不正な文字列が渡された場合の処理を実装する必要があります。

精度に関する問題

big.Float は任意の精度を持つことができますが、デフォルトの精度で計算を行うと、期待通りの精度が得られない場合があります。

  • 精度設定の誤り
    SetPrec() メソッドで精度を設定する際に、必要な精度よりも低い値を設定してしまうと、計算結果の精度が不足する可能性があります。

  • デフォルト精度の限界
    big.Float はデフォルトで math.MaxPrec 程度の精度を持ちますが、より高い精度が必要な計算では、明示的に精度を設定する必要があります。

トラブルシューティング

  • 計算の途中で精度が失われていないか確認するため、必要に応じて中間結果を出力して確認してください。

  • 高精度な計算が必要な場合は、SetPrec() メソッドを使用して適切な精度を設定してください。精度はビット数で指定します。

    f := big.NewFloat(0)
    f.SetPrec(100) // 100ビットの精度を設定
    

他の数値型との変換

big.Float と標準の数値型 (float64, int など) との間で変換を行う際に、精度が失われる可能性があります。

  • Int() などの整数変換
    big.Float を整数型に変換するメソッド (Int(), Int64(), Uint64() など) は、小数点以下を切り捨てます。意図しない結果になる可能性があるため注意が必要です。

  • Float64() メソッドの精度損失
    big.Float の値を float64 に変換する Float64() メソッドは、float64 の精度に丸められるため、情報が失われる可能性があります。

    f := new(big.Float).SetString("1.234567890123456789")
    f64, _ := f.Float64()
    fmt.Println("float64:", f64) // 精度が失われる可能性
    

トラブルシューティング

  • 整数への変換が必要な場合は、丸め処理 (Round(), Floor(), Ceil()) を適切に行うことを検討してください。
  • 精度が重要な場合は、big.Float のまま計算を進めることを検討してください。
  • 標準の数値型との変換は、精度が失われる可能性があることを理解した上で行ってください。

nil レシーバ

big.Float 型のポインタが nil の状態でメソッドを呼び出すと、ランタイムパニックが発生します。

var f *big.Float
// f.SetString("1.0") // パニックが発生

トラブルシューティング

  • big.Float 型のポインタを使用する場合は、必ず new(big.Float)big.NewFloat() で初期化してからメソッドを呼び出してください。

比較に関する注意点

big.Float の比較には、直接的な == 演算子ではなく、Cmp() メソッドを使用する必要があります。== 演算子はポインタの比較を行うため、値が同じでも false を返すことがあります。

f1 := big.NewFloat(1.0)
f2 := big.NewFloat(1.0)
fmt.Println(f1 == f2)    // 出力: false (ポインタの比較)
fmt.Println(f1.Cmp(f2)) // 出力: 0 (値が等しい)

トラブルシューティング

  • big.Float の値を比較する場合は、必ず Cmp() メソッドを使用してください。Cmp() は、レシーバが引数より大きい場合は 1、等しい場合は 0、小さい場合は -1 を返します。


例1: 基本的な生成と初期化

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 引数なしで生成(初期値は 0)
	f1 := big.NewFloat(0)
	fmt.Println("f1:", f1)

	// float64 型の値で初期化
	f2 := big.NewFloat(3.14159)
	fmt.Println("f2:", f2)

	// 文字列で初期化(高精度)
	f3 := new(big.Float)
	f3.SetString("1.61803398874989484820")
	fmt.Println("f3:", f3)

	// 既存の big.Float から新しい big.Float を生成(値のコピー)
	f4 := new(big.Float).Set(f3)
	fmt.Println("f4 (f3 からコピー):", f4)
}

この例では、big.NewFloat() を使っていくつかの big.Float 型の変数を生成し、異なる方法で初期化しています。

  • f4Set() メソッドを使って、既存の big.Float (f3) の値をコピーして初期化しています。
  • f3new(big.Float) でポインタを生成し、SetString() メソッドを使って文字列で初期化しています。これにより、float64 よりも高い精度で値を設定できます。
  • f2float64 型の値で初期化されます。
  • f1 は引数なしで生成され、初期値は 0 になります。

例2: 算術演算

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f1 := big.NewFloat(10.5)
	f2 := big.NewFloat(3.2)

	// 加算
	sum := new(big.Float).Add(f1, f2)
	fmt.Println("f1 + f2 =", sum)

	// 減算
	diff := new(big.Float).Sub(f1, f2)
	fmt.Println("f1 - f2 =", diff)

	// 乗算
	prod := new(big.Float).Mul(f1, f2)
	fmt.Println("f1 * f2 =", prod)

	// 除算
	quo := new(big.Float).Quo(f1, f2)
	fmt.Println("f1 / f2 =", quo)
}

この例では、big.Float 型の変数同士で基本的な算術演算 (Add, Sub, Mul, Quo) を行っています。演算の結果は、新しい big.Float 型の変数に格納されます。

例3: 精度設定

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f := new(big.Float).SetString("1.0")
	fmt.Printf("デフォルト精度: %d ビット\n", f.Prec())

	// 精度を 50 ビットに設定
	f.SetPrec(50)
	fmt.Printf("精度 50 ビット: %d ビット, 値: %s\n", f.Prec(), f.String())

	// より高い精度を設定
	f.SetPrec(100)
	fmt.Printf("精度 100 ビット: %d ビット, 値: %s\n", f.Prec(), f.String())
}

この例では、SetPrec() メソッドを使って big.Float の精度を設定する方法を示しています。Prec() メソッドで現在の精度(ビット数)を取得できます。精度を高く設定することで、より多くの桁数を正確に保持できます。

例4: 比較

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f1 := big.NewFloat(3.14)
	f2 := big.NewFloat(3.14159)
	f3 := big.NewFloat(3.14)

	fmt.Println("f1 と f2 の比較:", f1.Cmp(f2)) // -1 (f1 < f2)
	fmt.Println("f1 と f3 の比較:", f1.Cmp(f3)) // 0  (f1 == f3)
	fmt.Println("f2 と f1 の比較:", f2.Cmp(f1)) // 1  (f2 > f1)
}

この例では、Cmp() メソッドを使って big.Float 型の変数を比較しています。Cmp() メソッドは、レシーバが引数より大きい場合は 1、等しい場合は 0、小さい場合は -1 を返します。直接 == 演算子で比較しないように注意してください。

例5: 文字列との変換

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f := new(big.Float).SetPrec(64)
	f.SetString("123.4567890123456789")

	// big.Float を文字列に変換
	str := f.String()
	fmt.Println("big.Float -> string:", str)

	// 文字列から big.Float に変換 (例1で紹介済み)
}

この例では、String() メソッドを使って big.Float 型の値を文字列に変換する方法を示しています。SetString() を使うことで、文字列から big.Float に変換できます(例1を参照)。



new(big.Float) を使用して生成し、後から値を設定する

big.NewFloat() は生成と同時に初期値を設定する便利な方法ですが、単に big.Float 型のゼロ値のポインタを生成し、後からメソッドを使って値を設定することも一般的です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// new() で big.Float のポインタを生成(初期値はゼロ値)
	f := new(big.Float)
	fmt.Println("初期値:", f) // 出力: 初期値: 0

	// SetFloat64() で float64 型の値を設定
	f.SetFloat64(2.71828)
	fmt.Println("SetFloat64 後:", f)

	// SetString() で文字列から値を設定
	_, ok := f.SetString("3.1415926535")
	if ok {
		fmt.Println("SetString 後:", f)
	} else {
		fmt.Println("SetString エラー")
	}

	// SetInt64() で int64 型の値を設定
	f.SetInt64(12345)
	fmt.Println("SetInt64 後:", f)
}

この方法では、new(big.Float)*big.Float 型のゼロ値(値が 0 の big.Float を指すポインタ)を作成し、その後 SetFloat64(), SetString(), SetInt64() などの Set 系のメソッドを使って値を設定します。

利点

  • 複数の異なる型の値から初期化する場合に、一貫した方法で使用できます。
  • 生成と初期化を分離できるため、コードの可読性が向上する場合があります。

既存の big.Float 型の値からコピーする

すでに存在する big.Float 型の値をコピーして新しい big.Float を作成することもできます。これは、Set() メソッドを使用します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f1 := big.NewFloat(1.0)
	f2 := new(big.Float)

	// f1 の値を f2 にコピー
	f2.Set(f1)
	fmt.Println("f1:", f1)
	fmt.Println("f2 (f1 からコピー):", f2)

	// f2 を変更しても f1 に影響はない
	f2.Add(f2, big.NewFloat(0.5))
	fmt.Println("f1 (変更なし):", f1)
	fmt.Println("f2 (変更後):", f2)
}

この方法を使うと、既存の big.Float の状態を維持しつつ、それに基づいて新しい big.Float で計算などを行いたい場合に便利です。

利点

  • 元の値に影響を与えずに新しい値で操作を行えます。
  • 既存の big.Float の値を簡単に複製できます。

big.Float リテラル(直接的な初期化)は存在しない

Go 言語には、組み込みの数値型 (int, float64 など) のように、big.Float 型の値を直接リテラルで記述する方法はありません。常に big.NewFloat() 関数や SetString() などのメソッドを介して値を設定する必要があります。これは、big.Float が任意の精度を持つため、コンパイル時にその精度を決定できないためと考えられます。

カスタムの初期化関数を作成する(高度なケース)

特定のパターンで big.Float を初期化する場合、独自のヘルパー関数を作成することも考えられます。

package main

import (
	"fmt"
	"math/big"
)

// 特定の精度で "pi" の近似値を生成する関数
func NewPiFloat(precision uint) *big.Float {
	piStr := "3.14159265358979323846264338327950288419716939937510" // 例
	f := new(big.Float).SetPrec(precision)
	_, ok := f.SetString(piStr)
	if !ok {
		return big.NewFloat(0) // エラー処理
	}
	return f
}

func main() {
	pi50 := NewPiFloat(50)
	fmt.Printf("Pi (精度 50): %s (精度: %d)\n", pi50.String(), pi50.Prec())

	pi100 := NewPiFloat(100)
	fmt.Printf("Pi (精度 100): %s (精度: %d)\n", pi100.String(), pi100.Prec())
}

この例では、特定の精度で円周率の近似値を big.Float として生成する NewPiFloat() 関数を作成しています。これは、特定の定数や複雑な初期化ロジックを隠蔽し、コードをより読みやすくするために役立ちます。

big.NewFloat()big.Float 型の値を生成する基本的な方法ですが、

  • Set() メソッドを使用して既存の big.Float の値をコピーする方法
  • new(big.Float) を使用して生成し、SetFloat64(), SetString(), SetInt64() などのメソッドで後から値を設定する方法

も、状況によっては有効な代替手段となります。big.Float のリテラルは存在しないため、常に関数やメソッドを介して値を設定する必要があります。高度なケースでは、カスタムの初期化関数を作成することも検討できます。