精度が重要な計算に!Go言語 big.Float.Sub() の活用事例

2025-06-01

big.Float.Sub() は、Go の math/big パッケージで提供されている、任意精度の浮動小数点数を扱うための型である big.Float のメソッドの一つです。このメソッドは、2つの big.Float 型の値を減算し、その結果をレシーバ(メソッドを呼び出した big.Float 型の変数)に格納します。

具体的には、以下のような働きをします。

func (z *Float) Sub(x, y *Float) *Float

このメソッドのシグネチャ(関数の型)からわかるように、

  • *Float: メソッドは、結果を格納したレシーバ z へのポインタを返します。通常は、メソッドチェーンなどで結果をすぐに利用する場合に使われます。
  • (y *Float): 減算する数(減数)へのポインタです。
  • (x *Float): 減算される数(被減数)へのポインタです。
  • (z *Float): これはレシーバと呼ばれる部分で、減算の結果がこの big.Float 型の変数 z に格納されます。メソッドを呼び出す際には、この z のアドレス(&z のように)を指定します。

処理の流れ

z.Sub(x, y) という形でメソッドを呼び出すと、以下の処理が行われます。

  1. x が指す big.Float の値から、y が指す big.Float の値を減算します。
  2. その減算の結果を、z が指す big.Float に格納します。
  3. z へのポインタを返します。

使用例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(10.5)
	y := big.NewFloat(3.2)
	z := new(big.Float)

	// z = x - y を計算する
	result := z.Sub(x, y)

	fmt.Printf("%s - %s = %s\n", x.String(), y.String(), result.String()) // 出力: 10.5 - 3.2 = 7.3
	fmt.Printf("zの値: %s\n", z.String())                                  // 出力: zの値: 7.3
}

この例では、

  1. x に 10.5、y に 3.2 を持つ big.Float 型の変数をそれぞれ作成しています。
  2. z は新しい big.Float 型の変数として初期化されています。
  3. z.Sub(x, y) を呼び出すことで、x の値から y の値を減算し、その結果が z に格納されます。戻り値として z へのポインタが result に代入されます。
  4. 最後に、減算の結果と z の値を出力しています。
  • 引数 xybig.Float へのポインタである必要があることに注意してください。
  • Sub() メソッドは、レシーバの値を変更します。
  • big.Float は任意精度であるため、標準の float32float64 型よりも高い精度で浮動小数点数の計算を行うことができます。


引数が nil ポインタである

big.Float.Sub() の引数 xy*big.Float 型、つまり big.Float へのポインタである必要があります。もしこれらの引数に nil を渡してしまうと、プログラムはパニックを起こします。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	var x *big.Float // nil ポインタ
	y := big.NewFloat(3.2)
	z := new(big.Float)

	// x が nil なので、ここでパニックが発生する
	result := z.Sub(x, y)
	fmt.Println(result.String())
}

トラブルシューティング

  • 関数やメソッドから big.Float のポインタを受け取っている場合、そのポインタが nil でないことを事前に確認する(if x == nil { ... } のように)などの対策が必要です。
  • big.NewFloat() などを使用して、big.Float 型の変数を適切に初期化し、そのポインタを Sub() に渡しているか確認してください。

レシーバが nil ポインタである

big.Float.Sub() はレシーバ(メソッドを呼び出す側の big.Float 型の変数 z)へのポインタを通して結果を格納します。もしレシーバが nil ポインタの場合、メソッド呼び出し時にパニックが発生します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	var z *big.Float // nil ポインタ
	x := big.NewFloat(10.5)
	y := big.NewFloat(3.2)

	// z が nil なので、ここでパニックが発生する
	result := z.Sub(x, y)
	fmt.Println(result.String())
}

トラブルシューティング

  • 構造体の中に big.Float のポインタが含まれている場合、その構造体のインスタンスが正しく初期化されているか確認してください。
  • new(big.Float) を使用して big.Float の新しいインスタンスを作成し、そのポインタに対して Sub() を呼び出すようにしてください。

期待した精度での計算が行われていない

big.Float は任意精度を提供しますが、デフォルトの精度で計算が行われます。もしより高い精度での計算が必要な場合は、SetPrec() メソッドを使用して精度を設定する必要があります。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(1.0)
	y := big.NewFloat(0.9)
	z := new(big.Float)

	// デフォルトの精度で計算
	result1 := z.Sub(x, y)
	fmt.Printf("デフォルト精度: %s\n", result1.String()) // 結果は期待通りに見えることが多いですが...

	// より高い精度を設定して計算
	z.SetPrec(100)
	result2 := new(big.Float).Sub(x, y)
	result2.SetPrec(100)
	fmt.Printf("精度100: %s\n", result2.String())
}

トラブルシューティング

  • 計算の途中で精度が失われないように、関連する全ての big.Float 変数に対して適切な精度を設定することを検討してください。
  • 高い精度が必要な場合は、計算を行う前にレシーバや引数の big.Float 変数に対して SetPrec() を呼び出し、適切な精度を設定してください。

他の big.Float メソッドとの組み合わせによる予期せぬ結果

big.Float には Sub() 以外にも様々なメソッドがあります。これらのメソッドを組み合わせて使用する際に、予期せぬ結果が生じることがあります。例えば、精度が異なる big.Float 同士で演算を行う場合などです。

トラブルシューティング

  • 複雑な計算を行う場合は、段階的に結果を確認しながら進めることを推奨します。
  • メソッドの呼び出し順序や、各メソッドがレシーバや引数にどのような影響を与えるかを正確に理解してください。
  • big.Float 変数の精度を意識し、必要に応じて SetPrec() で調整してください。

文字列からの変換エラー (SetString() などを使用した場合)

big.Float の値を文字列から設定する際に (SetString() メソッドなど)、不正な形式の文字列を渡すとエラーが発生します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	f := new(big.Float)
	_, _, err := f.SetString("invalid-number")
	if err != nil {
		fmt.Println("文字列変換エラー:", err)
	}
}
  • 入力文字列が big.Float が解釈できる正しい形式(数値、小数点、指数表記など)であることを確認してください。
  • SetString() の戻り値であるエラー (err) を確認し、エラーが発生した場合は適切なエラー処理を行ってください。


基本的な減算

これは先ほども示した基本的な例です。2つの big.Float 型の数値を減算し、結果を表示します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(10.5)
	y := big.NewFloat(3.2)
	z := new(big.Float)

	// z = x - y を計算
	result := z.Sub(x, y)

	fmt.Printf("%s - %s = %s\n", x.String(), y.String(), result.String())
	fmt.Printf("zの値: %s\n", z.String())
}

メソッドチェーンでの利用

Sub() メソッドはレシーバへのポインタを返すため、メソッドチェーンで続けて他の操作を行うことができます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(10.0)
	y := big.NewFloat(3.0)
	a := big.NewFloat(2.0)
	b := new(big.Float)

	// b = (x - y) * a を計算
	result := b.Sub(x, y).Mul(b, a)

	fmt.Printf("(%s - %s) * %s = %s\n", x.String(), y.String(), a.String(), result.String())
	fmt.Printf("bの値: %s\n", b.String())
}

この例では、まず b.Sub(x, y)x - y の結果が b に格納され、その b のポインタが返されます。次に、返されたポインタに対して Mul(b, a) が呼び出され、(x - y) * a の結果が再び b に格納されます。

異なる精度での減算

big.Float は任意精度を持つため、必要に応じて精度を設定できます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(1.0)
	y := big.NewFloat(0.9)
	z1 := new(big.Float)
	z2 := new(big.Float)

	// デフォルト精度での減算
	result1 := z1.Sub(x, y)
	fmt.Printf("デフォルト精度: %s\n", result1.String())

	// より高い精度を設定して減算
	x.SetPrec(100)
	y.SetPrec(100)
	z2.SetPrec(100)
	result2 := z2.Sub(x, y)
	fmt.Printf("精度100: %s\n", result2.String())
}

この例では、デフォルト精度と、より高い精度(100ビット)での減算結果を比較しています。高い精度を設定することで、より正確な計算が可能になります。

ループ内での減算

ループの中で Sub() を使用して、値を累積的に減算する例です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	total := big.NewFloat(100.0)
	decrement := big.NewFloat(5.5)

	for i := 0; i < 5; i++ {
		total.Sub(total, decrement)
		fmt.Printf("ステップ %d: %s\n", i+1, total.String())
	}
}

この例では、初期値 100.0 から 5.5 を 5 回減算していく様子を示しています。

関数内で big.Float を扱う

関数内で big.Float を受け取り、減算処理を行う例です。

package main

import (
	"fmt"
	"math/big"
)

func subtractFloats(a, b *big.Float) *big.Float {
	result := new(big.Float)
	return result.Sub(a, b)
}

func main() {
	num1 := big.NewFloat(25.8)
	num2 := big.NewFloat(12.3)

	difference := subtractFloats(num1, num2)
	fmt.Printf("%s - %s = %s\n", num1.String(), num2.String(), difference.String())
}

この例では、subtractFloats という関数が2つの big.Float ポインタを受け取り、それらの差を計算して新しい big.Float ポインタを返します。



Add() メソッドと負の数を使う

減算は、負の数を加算することと同じです。したがって、減算したい数値を負の値に変換し、Add() メソッドを使用することで、Sub() と同じ結果を得ることができます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(10.5)
	y := big.NewFloat(3.2)
	negY := new(big.Float).Mul(y, big.NewFloat(-1.0)) // y に -1 を掛けて負の数にする
	z := new(big.Float)

	// z = x + (-y) を計算
	result := z.Add(x, negY)

	fmt.Printf("%s + (%s) = %s\n", x.String(), negY.String(), result.String())
	fmt.Printf("zの値: %s\n", z.String())
}

利点

  • Add() メソッドの理解を深めることができます。
  • 減算の概念を足し算で表現できるため、ロジックがよりシンプルに見える場合があります。

欠点

  • コードの意図が直接的ではなくなる可能性があります。
  • 直接的な減算ではないため、わずかに処理が増える可能性があります(負の数を生成するステップ)。

Set() メソッドと一時変数を使う

計算結果を直接レシーバに格納するのではなく、一時的な big.Float 変数に結果を格納し、その後 Set() メソッドでレシーバに値をコピーする方法です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewFloat(10.5)
	y := big.NewFloat(3.2)
	z := new(big.Float)
	temp := new(big.Float)

	// 一時変数 temp で減算結果を計算
	temp.Sub(x, y)

	// 結果を z にコピー
	z.Set(temp)

	fmt.Printf("%s - %s = %s\n", x.String(), y.String(), z.String())
}

利点

  • 複雑な計算の中間結果を管理しやすくなります。
  • 計算結果を一旦別の変数に保持できるため、元の変数を保持しておきたい場合に便利です。

欠点

  • 直接レシーバに格納するよりもステップが増えます。
  • 余分な変数 (temp) が必要になります。

Sub() を使った関数を作成する

直接的な代替方法ではありませんが、big.Float.Sub() をラップした独自の減算関数を作成することで、コードの可読性や再利用性を高めることができます。

package main

import (
	"fmt"
	"math/big"
)

func subtract(a, b *big.Float) *big.Float {
	result := new(big.Float)
	return result.Sub(a, b)
}

func main() {
	num1 := big.NewFloat(25.8)
	num2 := big.NewFloat(12.3)

	difference := subtract(num1, num2)
	fmt.Printf("%s - %s = %s\n", num1.String(), num2.String(), difference.String())
}

利点

  • 再利用性が高まります。
  • 特定の精度設定やエラーハンドリングなどを関数内で一元的に管理できます。
  • 減算処理を抽象化し、コードが読みやすくなります。

欠点

  • 基本的な減算処理に対して、追加の関数定義が必要になります。

標準の浮動小数点数型 (float64, float32) を使用する (精度に注意)

任意精度が必要ない場合や、パフォーマンスが重要な場合は、標準の float64float32 型を使用することも考えられます。ただし、これらの型は精度に限界があるため、注意が必要です。

package main

import (
	"fmt"
)

func main() {
	x := 10.5
	y := 3.2
	result := x - y

	fmt.Printf("%f - %f = %f\n", x, y, result)
}

利点

  • コードがより簡潔になります。
  • math/big パッケージよりも高速に動作する可能性があります。

欠点

  • 任意精度ではないため、計算結果に誤差が生じる可能性があります。特に、繰り返しの計算や非常に大きな数、非常に小さな数を扱う場合に顕著になります。
  • パフォーマンス
    厳密な精度が不要で、パフォーマンスが重要な場合(ただし、精度低下のリスクを理解しておく必要があります)。
  • コードの再利用性
    減算処理を特定の精度やエラーハンドリングと組み合わせて再利用したい場合。
  • 中間結果の保持
    計算の途中で元の値を保持しておきたい場合や、中間結果を明示的に管理したい場合。
  • ロジックの可読性
    負の数を加算する方が、特定の状況でロジックを理解しやすい場合。