【Go言語】big.Float.SetFloat64() 徹底解説:よくある落とし穴と対策
簡単に言うと、float64
の浮動小数点数を、より高い精度を持つ big.Float
型に変換して設定するために使われます。
big.Float
と float64
の違い
-
big.Float
:math/big
パッケージで提供される型で、任意精度の浮動小数点数を扱えます。float64
のような固定されたビット数に制限されず、必要に応じて精度(有効桁数)を設定できるため、非常に高い精度が求められる計算や、浮動小数点誤差を極力避けたい場合に利用されます。 -
float64
: Go言語の組み込み型で、IEEE 754 倍精度浮動小数点数(64ビット)で数値を表現します。多くのプログラミング言語で標準的に使われる浮動小数点数ですが、表現できる桁数には限りがあり、非常に大きい数や非常に小さい数、あるいは循環小数などを正確に表現できない場合があります。このため、計算によっては「浮動小数点誤差」と呼ばれるわずかな誤差が生じることがあります。
SetFloat64()
の役割
SetFloat64(x float64) *Float
メソッドは、レシーバーである *big.Float
オブジェクトに、引数 x
で与えられた float64
の値を設定します。
重要な点
- 初期化や値の再設定に便利:
big.Float
オブジェクトを特定のfloat64
の値で初期化したり、途中で値を変更したりする際に便利です。 float64
の精度で設定される:SetFloat64()
は、あくまで入力されたfloat64
の値(すでにfloat64
の精度で表現されている値)をbig.Float
に設定します。float64
自体が持つ精度以上の情報が付加されるわけではありません。つまり、もし元のfloat64
の値にすでに丸め誤差が含まれていれば、その誤差も含めてbig.Float
に設定されます。
使用例
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 の値
f64Value := 0.1 + 0.2 // float64 の性質上、正確には 0.3 にならない場合がある
// big.Float を作成
bf := new(big.Float)
// float64 の値を big.Float に設定
bf.SetFloat64(f64Value)
fmt.Printf("float64の値: %f\n", f64Value)
fmt.Printf("big.Floatに設定した値: %s\n", bf.String())
// 別の float64 の値で再設定
bf.SetFloat64(1.2345678901234567) // float64 の精度を超える部分もある
fmt.Printf("再設定後のbig.Floatの値: %s\n", bf.String())
// SetFloat64() とは異なる初期化方法(文字列からの設定)
// こちらの方が厳密な精度を維持しやすい
bfPrecise, _, _ := new(big.Float).SetPrec(100).Parse("0.123456789012345678901234567890123456789", 10)
fmt.Printf("文字列から設定したbig.Floatの値: %s\n", bfPrecise.String())
}
出力例
float64の値: 0.300000
big.Floatに設定した値: 0.30000000000000004
再設定後のbig.Floatの値: 1.2345678901234567
文字列から設定したbig.Floatの値: 0.123456789012345678901234567890123456789
この例からわかるように、0.1 + 0.2
の float64
の結果は 0.30000000000000004
のようにわずかな誤差を含みますが、SetFloat64()
はその誤差を含んだ値をそのまま big.Float
に設定します。
big.Float.SetFloat64()
は、既存の float64
の値を math/big
パッケージで計算するために big.Float
に変換したい場合に利用します。
float64 の精度限界に起因する誤解 (最も一般的)
エラーや誤解の内容
SetFloat64()
を使って float64
の値を big.Float
に設定すると、まるでbig.Float
がその値を「より正確」に表現してくれるかのように誤解しがちです。しかし、SetFloat64()
は入力された float64
の値をそのまま big.Float
にコピーするだけです。つまり、元の float64
が持っていた精度以上の情報が付加されることはありません。 float64
がすでに丸め誤差を含んでいる場合、その誤差も big.Float
に引き継がれます。
例
package main
import (
"fmt"
"math/big"
)
func main() {
f64Val := 0.1 + 0.2 // float64 の計算結果は正確に 0.3 ではないことが多い
bf := new(big.Float)
bf.SetFloat64(f64Val)
fmt.Printf("float64: %f\n", f64Val)
fmt.Printf("big.Float: %s\n", bf.String())
// 期待: big.Float: 0.3
// 実際: big.Float: 0.30000000000000004 (多くの場合)
}
トラブルシューティング/解決策
- 厳密な精度が必要な場合
- 文字列からの初期化
big.Float.SetString()
またはbig.Float.Parse()
を使用して、数値を文字列として渡し、その文字列からbig.Float
を初期化します。これにより、float64
の中間的な精度を介さずに、より高い精度で値を設定できます。bfPrecise := new(big.Float).SetPrec(50) // 必要な精度を設定 bfPrecise.SetString("0.3") // または Parse("0.3", 10) fmt.Printf("big.Float (from string): %s\n", bfPrecise.String()) // 0.3
- big.Rat の利用
有理数(分数)で値を表現できるbig.Rat
を利用し、そこからbig.Float
に変換することで、循環小数などを厳密に扱うことができます。r := new(big.Rat).SetFrac64(3, 10) // 3/10 bfFromRat := new(big.Float).SetPrec(50).SetRat(r) fmt.Printf("big.Float (from big.Rat): %s\n", bfFromRat.String())
- 文字列からの初期化
- 根本的な理解
SetFloat64()
は「既存のfloat64
の値をbig.Float
の形式に変換する」ものであり、「float64
の精度問題を解決する」ものではないと理解してください。
big.Float の精度設定 (Prec) の影響
エラーや誤解の内容
big.Float
は任意精度ですが、デフォルトの精度(約64ビット、float64
相当)で初期化されることが多いです。SetFloat64()
を呼び出す前に SetPrec()
で高い精度を設定していないと、計算結果が期待通りにならない場合があります。
例
float64
では表現できない桁数の数値を SetFloat64()
で設定しても、事前に精度を設定していなければ、デフォルトの精度で丸められてしまいます。
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 では表現しきれないほど長い小数
longFloat64 := 1.234567890123456789 // 18桁目以降は float64 では失われる情報
bfDefaultPrec := new(big.Float)
bfDefaultPrec.SetFloat64(longFloat64)
fmt.Printf("デフォルト精度 big.Float: %s\n", bfDefaultPrec.String()) // 1.2345678901234567 とかになる
// 事前に精度を設定
bfHighPrec := new(big.Float).SetPrec(100) // 100ビットの精度を設定
bfHighPrec.SetFloat64(longFloat64) // ここで設定されるのは float64 の丸められた値
fmt.Printf("高精度 big.Float (SetFloat64): %s\n", bfHighPrec.String()) // 同上
// 精度を活かすには SetString などを使う
bfHighPrecFromString := new(big.Float).SetPrec(100)
bfHighPrecFromString.SetString("1.234567890123456789")
fmt.Printf("高精度 big.Float (SetString): %s\n", bfHighPrecFromString.String()) // 1.234567890123456789 となる
}
トラブルシューティング/解決策
- SetFloat64() は入力が float64 である限り限界がある
繰り返しになりますが、SetFloat64()
は入力がfloat64
であるため、高精度なbig.Float
に設定しても、元のfloat64
が持っていなかった情報は得られません。big.Float
の高精度を本当に活かすには、文字列からのパース (SetString
やParse
) や、他のbig.Float
やbig.Rat
からの変換を使用する必要があります。 - 目的の精度を設定
SetFloat64()
を呼び出す前に、new(big.Float).SetPrec(n)
のようにして、目的の精度n
を設定してください。
ポインタレシーバーの理解不足
エラーや誤解の内容
SetFloat64()
は *big.Float
型のレシーバーを持つメソッドです。これは、メソッドが元の big.Float
オブジェクトの値を直接変更することを意味します。しかし、new(big.Float)
を呼び出すことを忘れたり、値渡しで関数に渡してしまったりすると、期待通りの動作にならないことがあります。
例 (間違った使い方)
package main
import (
"fmt"
"math/big"
)
func main() {
// 間違い: var bf big.Float はゼロ値の big.Float を作成するが、
// メソッドを呼び出すにはポインタが必要
// bf.SetFloat64(1.0) // これはコンパイルエラーになる可能性があるか、panicを起こす
// 正しい使い方:
var bf big.Float
ptrBf := &bf // ポインタを取得してメソッドを呼び出す
ptrBf.SetFloat64(1.0)
fmt.Printf("正しい使い方: %s\n", ptrBf.String())
// より一般的な正しい使い方
bf2 := new(big.Float) // new() はポインタを返す
bf2.SetFloat64(2.0)
fmt.Printf("一般的な正しい使い方: %s\n", bf2.String())
}
トラブルシューティング/解決策
- 常にポインタを使用
big.Float
のメソッドを呼び出す際は、new(big.Float)
で作成したポインタ、または既存のbig.Float
のアドレス (&myFloat
) を使用してください。
エラーや誤解の内容
big.Float
のゼロ値は 0
です。SetFloat64(0.0)
を呼び出すと、big.Float
はゼロになります。これは当然の動作ですが、他の値で初期化するつもりだったのに誤って 0.0
を設定してしまい、意図しない結果になることがあります。
- 意図しない初期化の確認
変数に値を設定する際に、本当に0.0
を設定したいのか、それとも他の値を設定したいのかを意識してください。
例1: float64
の値を big.Float
に設定する基本
最も基本的な使い方です。float64
型の変数に格納された値を big.Float
に変換して設定します。
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 の値
f64Value := 3.1415926535
// big.Float を作成
// new(big.Float) は big.Float のゼロ値へのポインタを返します
bf := new(big.Float)
// SetFloat64() を使って float64 の値を big.Float に設定
bf.SetFloat64(f64Value)
fmt.Printf("float64 の値: %f\n", f64Value)
fmt.Printf("big.Float に設定した値: %s\n", bf.String())
// %s を使うことで big.Float の正確な文字列表現が得られます
}
実行結果例
float64 の値: 3.141593
big.Float に設定した値: 3.1415926535
例2: float64
の精度限界と big.Float
の関係
SetFloat64()
は、元の float64
が持っていた精度以上の情報を提供するわけではありません。float64
の計算で生じる誤差もそのまま引き継がれます。
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 で 0.1 + 0.2 を計算すると、わずかな誤差が生じることがあります
f64Sum := 0.1 + 0.2
// big.Float を作成
bf := new(big.Float)
// SetFloat64() で設定
bf.SetFloat64(f64Sum)
fmt.Printf("float64 (0.1 + 0.2) の結果: %f\n", f64Sum)
fmt.Printf("big.Float に設定した値: %s\n", bf.String())
// 参考: 厳密な 0.3 を big.Float に設定する方法
bfPrecise := new(big.Float).SetPrec(50) // 精度を設定
bfPrecise.SetString("0.3") // 文字列から設定
fmt.Printf("big.Float (厳密な0.3): %s\n", bfPrecise.String())
}
実行結果例
float64 (0.1 + 0.2) の結果: 0.300000
big.Float に設定した値: 0.30000000000000004 // float64 の誤差が引き継がれる
big.Float (厳密な0.3): 0.3
この例から、SetFloat64()
は float64
の値を big.Float
に「変換」するものであり、float64
が持つ誤差を「修正」するものではないことがわかります。
例3: big.Float
の精度設定と SetFloat64()
big.Float
は任意精度ですが、SetFloat64()
を使う場合は、設定される値が float64
の精度に依存するため、事前に高い精度を設定しても、float64
が持つ情報量を超える値を表現できるわけではありません。
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 で表現しきれない長い小数
f64Long := 1.234567890123456789 // float64 の精度では、このうちの一部しか保持できない
// 1. デフォルト精度 (約64ビット) の big.Float
bfDefault := new(big.Float)
bfDefault.SetFloat64(f64Long)
fmt.Printf("デフォルト精度 big.Float (SetFloat64): %s\n", bfDefault.String())
// 2. 高精度 (100ビット) の big.Float
bfHighPrec := new(big.Float).SetPrec(100) // 精度を100ビットに設定
bfHighPrec.SetFloat64(f64Long) // ここで設定されるのは、f64Long の丸められた値
fmt.Printf("高精度 big.Float (SetFloat64): %s\n", bfHighPrec.String())
// 3. 高精度を活かすために文字列から設定する場合
bfHighPrecFromString := new(big.Float).SetPrec(100)
bfHighPrecFromString.SetString("1.234567890123456789") // 文字列から直接高精度で設定
fmt.Printf("高精度 big.Float (SetString): %s\n", bfHighPrecFromString.String())
}
実行結果例
デフォルト精度 big.Float (SetFloat64): 1.2345678901234567
高精度 big.Float (SetFloat64): 1.2345678901234567 // float64 の精度で設定される
高精度 big.Float (SetString): 1.234567890123456789 // 文字列から直接設定すると高精度が活かされる
この例は、SetFloat64()
が入力の float64
の精度に制限されることを明確に示しています。big.Float
の真の任意精度を活用するには、文字列からの初期化 (SetString
) や、他の big.Float
や big.Rat
からの変換がしばしば必要になります。
途中の計算結果を float64
で受け取り、それを big.Float
に変換して後続の高精度計算に利用するようなケースです。
package main
import (
"fmt"
"math/big"
)
func main() {
// float64 での初期計算
f64Result := 10.0 / 3.0 // 3.3333... となるが float64 では丸められる
fmt.Printf("float64 の初期計算結果: %f\n", f64Result)
// big.Float に変換して、さらに高精度な計算を続ける
bf1 := new(big.Float).SetPrec(100) // 高い精度を設定
bf1.SetFloat64(f64Result) // float64 の結果を設定
// big.Float での追加計算 (例: 3を掛ける)
bf3 := big.NewFloat(3.0) // big.Float の 3 を作成
bfResult := new(big.Float).SetPrec(100)
bfResult.Mul(bf1, bf3) // bf1 * bf3 を計算
fmt.Printf("big.Float に変換後の追加計算結果: %s\n", bfResult.String())
// 厳密な 10.0 / 3.0 から計算した場合
bfStrict := new(big.Float).SetPrec(100)
bfStrict.Quo(big.NewFloat(10.0), big.NewFloat(3.0)) // 10 / 3 を高精度で計算
bfStrict.Mul(bfStrict, big.NewFloat(3.0))
fmt.Printf("厳密な 10/3 から計算した場合の結果: %s\n", bfStrict.String())
}
実行結果例
float64 の初期計算結果: 3.333333
big.Float に変換後の追加計算結果: 9.999999999999999000000000000000000000000000000000000000000000000000000000000000000000000000000000000 // float64 の誤差が引き継がれる
厳密な 10/3 から計算した場合の結果: 10.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 // 厳密な計算
この例は、SetFloat64()
が float64
から big.Float
への「ゲートウェイ」として機能することを示していますが、一度 float64
の精度に落ちた情報は、big.Float
に変換してもそれ以上にはならないという重要な点を示しています。
big.Float.SetString(s string) (*Float, bool) / big.Float.Parse(s string, base int) (*Float, int, error)
特徴
- SetPrec() と組み合わせる
変換前にbig.Float
の精度をSetPrec()
で設定することで、文字列が持つ情報量のうち、必要な桁数までを保持できます。 - 文字列として数値を指定
数値を文字列リテラルとして直接指定することで、コンパイラやfloat64
の丸め誤差を介さずに、意図した通りの精度でbig.Float
を初期化できます。 - 最も推奨される代替手段
ほとんどのケースで、float64
の精度問題を回避し、正確な任意精度の浮動小数点数をbig.Float
に設定するのに最適な方法です。
利用例
package main
import (
"fmt"
"math/big"
)
func main() {
// (1) SetString() の利用
// 厳密な 0.3 を設定したい場合
bf1 := new(big.Float).SetPrec(50) // 精度を50ビットに設定
bf1.SetString("0.3")
fmt.Printf("SetString(\"0.3\"): %s\n", bf1.String()) // 0.3
// float64 では表現しきれない桁数を持つ数を設定
bf2 := new(big.Float).SetPrec(100) // さらに高い精度を設定
bf2.SetString("1.2345678901234567890123456789")
fmt.Printf("SetString(\"long decimal\"): %s\n", bf2.String())
// (2) Parse() の利用 (SetString よりも柔軟)
// 基数(base)を指定できる
bf3 := new(big.Float).SetPrec(50)
strValue := "1.25e+3" // 指数表記
_, _, err := bf3.Parse(strValue, 10) // 10進数でパース
if err != nil {
fmt.Println("Parse error:", err)
}
fmt.Printf("Parse(\"%s\"): %s\n", strValue, bf3.String()) // 1250
bf4 := new(big.Float).SetPrec(50)
hexValue := "0x1.8p+3" // 16進浮動小数点表記 (Go言語のfloat64でも使われる)
_, _, err = bf4.Parse(hexValue, 0) // baseを0にするとGo言語の浮動小数点リテラル規則に従う
if err != nil {
fmt.Println("Parse error:", err)
}
fmt.Printf("Parse(\"%s\"): %s\n", hexValue, bf4.String()) // 12
}
big.Float.SetInt(i *big.Int) *Float / big.Float.SetInt64(i int64) *Float
特徴
int64
で足りない場合は*big.Int
を使用します。- 整数からの変換
整数(*big.Int
またはint64
)をbig.Float
に変換します。整数は誤差なく浮動小数点数として表現できます。
利用例
package main
import (
"fmt"
"math/big"
)
func main() {
// (1) SetInt64() の利用
i64Value := int64(123456789012345)
bf1 := new(big.Float)
bf1.SetInt64(i64Value)
fmt.Printf("SetInt64(%d): %s\n", i64Value, bf1.String())
// (2) SetInt() の利用 (より大きな整数に対応)
biValue := new(big.Int)
biValue.SetString("98765432109876543210", 10) // 非常に大きな整数
bf2 := new(big.Float)
bf2.SetInt(biValue)
fmt.Printf("SetInt(%s): %s\n", biValue.String(), bf2.String())
}
big.Float.SetRat(r *big.Rat) *Float
特徴
- 金融計算など、正確な分数表現が重要な場面で役立ちます。
- 循環小数や厳密な分数
big.Rat
を利用すると、1/3
のような循環小数も厳密に表現でき、それをbig.Float
に変換する際に、設定された精度で可能な限り正確に丸められます。 - 有理数からの変換
math/big
パッケージのbig.Rat
(有理数、分数) からbig.Float
に変換します。
利用例
package main
import (
"fmt"
"math/big"
)
func main() {
// 1/3 を big.Rat で表現
r := new(big.Rat).SetFrac64(1, 3) // 分子1、分母3
fmt.Printf("big.Rat (1/3): %s\n", r.String())
// big.Float に変換 (高精度で丸められる)
bf := new(big.Float).SetPrec(100) // 精度を100ビットに設定
bf.SetRat(r)
fmt.Printf("SetRat(1/3) to big.Float: %s\n", bf.String())
// 1/7 の例
r2 := new(big.Rat).SetFrac64(1, 7)
bf2 := new(big.Float).SetPrec(100)
bf2.SetRat(r2)
fmt.Printf("SetRat(1/7) to big.Float: %s\n", bf2.String())
}
実行結果例
big.Rat (1/3): 1/3
SetRat(1/3) to big.Float: 0.3333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333
big.Rat (1/7): 1/7
SetRat(1/7) to big.Float: 0.1428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571428571429
特徴
SetPrec()
は含まれません。返されるbig.Float
はデフォルトの精度(約64ビット)で初期化されます。- 新しい big.Float を作成して値を設定
既存のbig.Float
に値を設定するのではなく、float64
の値を持つ新しいbig.Float
オブジェクトを返します。 - これは
big.Float.SetFloat64()
のように見えるかもしれませんが、厳密にはbig.Float
のメソッドではなく、math/big
パッケージのヘルパー関数です。
package main
import (
"fmt"
"math/big"
)
func main() {
f64Value := 123.456
// big.NewFloat() で新しい big.Float を作成し、値を設定
bf := big.NewFloat(f64Value)
fmt.Printf("big.NewFloat(%f): %s\n", f64Value, bf.String())
// デフォルト精度であるため、長い小数だと丸められる
bfLong := big.NewFloat(1.234567890123456789)
fmt.Printf("big.NewFloat(long): %s\n", bfLong.String())
}
メソッド/関数 | 入力形式 | 主な用途 | 精度に関する注意点 |
---|---|---|---|
SetFloat64() | float64 | 既存の big.Float に float64 値を設定 | float64 の精度限界を継承。より高精度にはならない。 |
SetString() | string | 推奨: 厳密な数値表現、高精度な初期化 | SetPrec() と組み合わせることで任意精度を活かせる。 |
Parse() | string , int | SetString より柔軟な文字列パース (基数など) | SetPrec() と組み合わせることで任意精度を活かせる。 |
SetInt() / SetInt64() | *big.Int , int64 | 整数を浮動小数点数として表現する | 整数は誤差なく表現できる。big.Float の精度で保持される。 |
SetRat() | *big.Rat | 有理数からの変換、厳密な分数表現 | SetPrec() と組み合わせることで、分数を精度で丸められる。 |
big.NewFloat() | float64 | float64 から新しい big.Float を作成 | デフォルト精度 (約64ビット) で作成される。 |