Go言語 math/bigとは?任意精度演算の基本と活用例

2025-06-01

"math/big" パッケージは、Goの標準ライブラリの一つで、任意精度の整数 (arbitrary-precision integers)任意精度の有理数 (arbitrary-precision rationals) を扱うための機能を提供します。

通常、Goの組み込みの整数型 (int, int64 など) や浮動小数点数型 (float64 など) は、扱える数値の範囲に制限があります。たとえば、int64 型で扱える整数の範囲は約 -9 * 10^18 から 9 * 10^18 までです。これよりも大きな数や、より正確な計算が必要な場合には、"math/big" パッケージの型を利用します。

"math/big" パッケージの主な型は以下の通りです。

  • Rat: 任意精度の有理数(分数)を表します。分子と分母を Int 型で保持しているため、丸め誤差なしに正確な有理数演算が可能です。
  • Int: 任意精度の整数を表します。事実上、メモリの許す限りどんなに大きな整数でも扱うことができます。

これらの型を使うことで、以下のような場面で役立ちます。

  • 高精度の計算: 標準の浮動小数点数型よりも高い精度での計算が必要な場合(ただし、Rat 型は有理数のみを扱います)。
  • 正確な有理数演算: 金融計算、記号処理など、わずかな丸め誤差も許容できない計算を行う場合。
  • 非常に大きな整数の計算: 暗号処理、組み合わせ数学、天文学など、標準の整数型では扱いきれない大きな数を扱う場合。

"math/big" パッケージでは、これらの Int 型や Rat 型に対する四則演算(加算、減算、乗算、除算)や、比較、ビット演算、べき乗、剰余演算など、様々な操作を行うためのメソッドが提供されています。

簡単な例 (Int 型の利用)

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 非常に大きな整数を生成
	a := big.NewInt(1234567890123456789)
	b := big.NewInt(9876543210987654321)

	// 加算
	sum := new(big.Int)
	sum.Add(a, b)
	fmt.Println("Sum:", sum)

	// 乗算
	product := new(big.Int)
	product.Mul(a, b)
	fmt.Println("Product:", product)

	// べき乗 (a の 2 乗)
	power := new(big.Int)
	power.Exp(a, big.NewInt(2), nil) // nil は modulus (剰余) を指定しない場合
	fmt.Println("Power:", power)
}

この例では、big.NewInt() 関数を使って big.Int 型の変数を初期化し、Add()Mul()Exp() などのメソッドを使って演算を行っています。演算結果を格納するための big.Int 型の変数を new(big.Int) で作成し、メソッドのレシーバとして使用している点に注意してください。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 有理数を生成 (分子/分母)
	r1 := big.NewRat(1, 3)   // 1/3
	r2 := big.NewRat(2, 5)   // 2/5

	// 加算
	sum := new(big.Rat)
	sum.Add(r1, r2)
	fmt.Println("Sum:", sum) // 出力: 11/15

	// 乗算
	product := new(big.Rat)
	product.Mul(r1, r2)
	fmt.Println("Product:", product) // 出力: 2/15
}


  1. 初期化忘れ (Nil Pointer Panic)

    • エラー: big.Intbig.Rat 型の変数を var で宣言した場合、初期値は nil になります。nil のポインタに対してメソッドを呼び出そうとすると、ランタイムエラー(panic)が発生します。
    • :
      var n *big.Int
      n.Add(n, big.NewInt(1)) // panic: runtime error: invalid memory address or nil pointer dereference
      
    • 解決策: big.NewInt()big.NewRat() を使って明示的にインスタンスを生成してから使用します。演算結果を格納する変数も同様に new(big.Int)new(big.Rat) で初期化するか、レシーバが nil でないことを確認します。
      n := new(big.Int)
      one := big.NewInt(1)
      n.Add(n, one)
      fmt.Println(n) // 出力: 1
      
  2. 演算結果の格納忘れ

    • エラー: math/big の多くの演算メソッドは、レシーバ自身を変更するのではなく、結果を別の big.Int または big.Rat 型の変数に格納します。結果を格納する変数を適切に用意しないと、期待した値が得られません。
    • :
      a := big.NewInt(5)
      b := big.NewInt(3)
      a.Add(a, b) // a は 8 にならない
      fmt.Println(a) // 出力: 5
      
    • 解決策: 演算結果を格納するための新しい big.Int または big.Rat 型の変数を用意し、メソッドのレシーバとして使用するか、メソッドの戻り値を使用します。
      a := big.NewInt(5)
      b := big.NewInt(3)
      sum := new(big.Int)
      sum.Add(a, b)
      fmt.Println("Sum:", sum) // 出力: Sum: 8
      
      // または
      a.Add(a, b) // この場合、a の値は変更される (レシーバ自身を変更するメソッドもある)
      fmt.Println(a) // 出力: 8 (Add のドキュメントを確認することが重要)
      
      多くの算術演算メソッドは、結果をレシーバに格納する形式 (a.Add(a, b)) と、新しい big.Int を返却する形式 (sum := big.NewInt(0).Add(a, b)) の両方を提供しています。ドキュメントをよく確認しましょう。
  3. 型の不一致

    • エラー: big.Intbig.Rat は異なる型です。これらの間で直接演算を行うことはできません。
    • 解決策: 必要に応じて、型変換を行います。big.Int から big.Rat を作成するには big.NewRat(n, 1) を使用します。逆方向の変換は情報が失われる可能性があるため、注意が必要です (rat.Num() で分子、rat.Denom() で分母を取得できます)。
  4. 大きな数の文字列変換のコスト

    • 注意点: 非常に大きな big.Intbig.Rat を文字列に変換 (String() メソッドなど) する場合、処理に時間がかかることがあります。特に頻繁に変換を行う場合は、パフォーマンスに影響を与える可能性があります。
    • トラブルシューティング: デバッグやログ出力など、本当に必要な場合にのみ文字列変換を行い、頻繁な変換は避けるようにします。
  5. 無限ループや極端に長い計算時間

    • 注意点: 非常に大きな数を扱う場合、演算によっては計算時間が非常に長くなる可能性があります。特に、指数関数的な計算や複雑なアルゴリズムを実装する際には注意が必要です。
    • トラブルシューティング: アルゴリズムの効率性を見直したり、必要に応じてタイムアウト処理などを実装したりすることを検討します。
  6. SetString の誤用

    • エラー: SetString(s string, base int) メソッドで文字列から big.Intbig.Rat を生成する際、文字列の形式や基数が正しくないとエラーが発生します。
    • 解決策: 文字列が指定された基数で正しく表現されているか確認します。SetString は成功した場合にレシーバ自身を返し、失敗した場合は nil を返します。戻り値をチェックしてエラーハンドリングを行うことが重要です。big.Rat の場合は、"分子/分母" の形式である必要があります。
  7. 比較演算子の誤用

    • エラー: big.Intbig.Rat の比較には、組み込みの ==, >, < などの演算子は使用できません。これらの型はポインタ型であるため、ポインタの値(メモリ上のアドレス)を比較してしまい、数値としての比較になりません。
    • 解決策: Cmp() メソッドを使用します。a.Cmp(b) は、a > b の場合に 1、a == b の場合に 0、a < b の場合に -1 を返します。
      a := big.NewInt(5)
      b := big.NewInt(5)
      if a.Cmp(b) == 0 {
          fmt.Println("a と b は等しい")
      }
      
  8. 精度に関する誤解 (Rat 型)

    • 注意点: big.Rat 型は有理数を正確に表現しますが、無理数(例えば 2や π)を正確に表現することはできません。これらの数値を扱う場合は、ある程度の精度で近似する必要があります。
    • トラブルシューティング: 必要な精度を考慮し、適切な近似方法を用いるか、他のライブラリ(例えば、浮動小数点数の拡張精度ライブラリ)の利用を検討します。


非常に大きな整数の加算と乗算

これは先ほども少し触れましたが、標準の整数型では扱えない大きな数の演算の基本的な例です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 非常に大きな整数を文字列で初期化
	num1Str := "123456789012345678901234567890"
	num2Str := "987654321098765432109876543210"

	num1 := new(big.Int)
	_, ok := num1.SetString(num1Str, 10) // 10進数として解析
	if !ok {
		fmt.Println("num1 の文字列解析に失敗しました")
		return
	}

	num2 := new(big.Int)
	_, ok = num2.SetString(num2Str, 10)
	if !ok {
		fmt.Println("num2 の文字列解析に失敗しました")
		return
	}

	// 加算
	sum := new(big.Int)
	sum.Add(num1, num2)
	fmt.Printf("%s + %s = %s\n", num1Str, num2Str, sum.String())

	// 乗算
	product := new(big.Int)
	product.Mul(num1, num2)
	fmt.Printf("%s * %s = %s\n", num1Str, num2Str, product.String())
}

この例では、文字列で非常に大きな整数を表現し、SetString メソッドを使って big.Int 型の変数に変換しています。その後、Add メソッドで加算、Mul メソッドで乗算を行い、結果を String() メソッドで文字列として出力しています。

階乗の計算

big.Int を使うことで、標準の整数型ではすぐにオーバーフローしてしまうような大きな数の階乗も計算できます。

package main

import (
	"fmt"
	"math/big"
)

func factorial(n int64) *big.Int {
	if n < 0 {
		return big.NewInt(0) // 負の数の階乗は定義しない
	}
	if n == 0 {
		return big.NewInt(1)
	}
	result := big.NewInt(1)
	for i := int64(1); i <= n; i++ {
		result.Mul(result, big.NewInt(i))
	}
	return result
}

func main() {
	n := int64(50)
	fact := factorial(n)
	fmt.Printf("%d! = %s\n", n, fact.String())
}

この例では、与えられた整数の階乗を計算する factorial 関数を実装しています。計算の途中で big.Int を使用することで、大きな階乗の値も正確に保持できます。

べき乗の計算

Exp メソッドを使うと、大きな数のべき乗計算も効率的に行えます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	base := big.NewInt(2)
	exponent := big.NewInt(100)
	modulus := big.NewInt(1000000007) // 剰余演算が必要ない場合は nil

	result := new(big.Int)
	result.Exp(base, exponent, modulus) // (base ^ exponent) mod modulus

	fmt.Printf("%s ^ %s mod %s = %s\n", base.String(), exponent.String(), modulus.String(), result.String())

	// 剰余演算なしのべき乗
	resultNoMod := new(big.Int)
	resultNoMod.Exp(base, exponent, nil)
	fmt.Printf("%s ^ %s = %s\n", base.String(), exponent.String(), resultNoMod.String())
}

Exp メソッドは、第三引数に modulus を指定することで、べき乗の結果をある数で割った余りを効率的に計算できます。これは、暗号処理などでよく使われるテクニックです。

有理数の計算

big.Rat 型を使うと、分数の形で正確な計算ができます。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 1/3 を作成
	r1 := big.NewRat(1, 3)
	fmt.Printf("r1 = %s\n", r1.String())

	// 2/5 を作成
	r2 := big.NewRat(2, 5)
	fmt.Printf("r2 = %s\n", r2.String())

	// 加算
	sum := new(big.Rat)
	sum.Add(r1, r2)
	fmt.Printf("%s + %s = %s\n", r1.String(), r2.String(), sum.String())

	// 乗算
	product := new(big.Rat)
	product.Mul(r1, r2)
	fmt.Printf("%s * %s = %s\n", r1.String(), r2.String(), product.String())

	// 比較
	if r1.Cmp(r2) < 0 {
		fmt.Printf("%s < %s\n", r1.String(), r2.String())
	} else if r1.Cmp(r2) > 0 {
		fmt.Printf("%s > %s\n", r1.String(), r2.String())
	} else {
		fmt.Printf("%s == %s\n", r1.String(), r2.String())
	}
}

この例では、big.NewRat で有理数を作成し、AddMul メソッドで演算を行っています。また、Cmp メソッドを使って有理数の比較を行っています。

Int と Rat の相互変換

Int 型と Rat 型を相互に変換する方法の例です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// big.Int から big.Rat を作成
	integer := big.NewInt(10)
	rationalFromInt := new(big.Rat).SetInt(integer)
	fmt.Printf("Integer: %s, Rational from Integer: %s\n", integer.String(), rationalFromInt.String())

	// big.Rat から big.Int (分子を取得)
	rational := big.NewRat(7, 3)
	numerator := new(big.Int).Num(rational)
	fmt.Printf("Rational: %s, Numerator: %s\n", rational.String(), numerator.String())

	// big.Rat から big.Int (整数部を取得)
	integerPart := new(big.Int).Num(new(big.Rat).Set(rational).Quo(rational, big.NewRat(1, 1))) // ちょっと回りくどいですが...
	fmt.Printf("Rational: %s, Integer Part: %s\n", rational.String(), integerPart.String())

	// より簡単な整数部取得 (Trunc)
	truncatedInt := new(big.Int).Set(rational.Num())
	truncatedInt.Div(truncatedInt, rational.Denom())
	fmt.Printf("Rational: %s, Truncated Integer Part: %s\n", rational.String(), truncatedInt.String())
}


外部の任意精度演算ライブラリの利用

Goのエコシステムには、標準ライブラリ以外にも任意精度演算を提供するサードパーティ製のライブラリが存在します。これらのライブラリは、"math/big" よりも特定の用途に特化していたり、異なるパフォーマンス特性を持っていたりする場合があります。

  • 他の任意精度整数/有理数ライブラリ: コミュニティによって開発されている、"math/big" と同様の機能を提供するライブラリも存在する可能性があります。特定のパフォーマンス要件や追加機能が必要な場合に探してみると良いでしょう。ただし、標準ライブラリに比べてドキュメントやサポートが限られる場合があることに注意が必要です。

  • github.com/ericlagergren/decimal: 任意精度の浮動小数点数(decimal)演算に特化したライブラリです。金融計算など、厳密な小数演算が求められる場合に適しています。IEEE 754 decimal 標準に準拠しています。

    package main
    
    import (
        "fmt"
        "github.com/ericlagergren/decimal"
    )
    
    func main() {
        d1, _ := decimal.NewFromString("1.23")
        d2, _ := decimal.NewFromString("4.56")
    
        sum := new(decimal.Big).Add(d1, d2)
        fmt.Println("Sum:", sum) // Output: Sum: 5.79
    }
    

他のプログラミング言語の利用と連携

Goで任意精度演算が複雑になる場合や、特定のライブラリが利用できない場合は、任意精度演算に強い他のプログラミング言語(例えば Python の decimalgmpy2 など)で計算を行い、その結果をGoのプログラムに連携させるという方法も考えられます。

  • 外部コマンドの実行: Goの os/exec パッケージを使って、他の言語で書かれたスクリプトを実行し、その出力をGoのプログラムで処理する。
  • gRPC や REST API: Goのプログラムから、別の言語で実装されたサービスにネットワーク経由でリクエストを送り、計算結果を受け取る。

この方法は、システム全体の複雑性を増す可能性がありますが、特定の計算に特化した強力なライブラリを利用できるというメリットがあります。

特定の用途に特化した実装

完全に汎用的な任意精度演算が必要ない場合は、特定の用途に合わせてより効率的な実装を検討できる場合があります。

  • 特定のアルゴリズムに最適化された実装: 例えば、非常に大きな整数の素数判定など、特定の数学的な問題に対しては、"math/big" を直接使うよりも効率的なアルゴリズムが存在する場合があります。これらのアルゴリズムをGoで直接実装することも選択肢の一つです。

  • 固定小数点数: 小数点以下の桁数を固定することで、整数演算に近い効率で小数演算を行うことができます。金融計算など、桁数が事前にわかっている場合に有効です。ただし、扱える数値の範囲や精度は固定されます。

    package main
    
    import "fmt"
    
    type FixedPoint int64
    
    const scaleFactor int64 = 100 // 2桁の小数点以下を表現
    
    func NewFixedPoint(val float64) FixedPoint {
        return FixedPoint(val * float64(scaleFactor))
    }
    
    func (fp FixedPoint) Float64() float64 {
        return float64(fp) / float64(scaleFactor)
    }
    
    func (fp FixedPoint) Add(other FixedPoint) FixedPoint {
        return fp + other
    }
    
    func main() {
        fp1 := NewFixedPoint(1.23)
        fp2 := NewFixedPoint(4.56)
        sum := fp1.Add(fp2)
        fmt.Println("Sum:", sum.Float64()) // Output: Sum: 5.79
    }
    

ハードウェアアクセラレーションの利用 (限定的)

非常に高度な数値計算の領域では、GPUなどのハードウェアアクセラレーションを利用することが考えられますが、Goの標準ライブラリや一般的なサードパーティ製のライブラリで直接的にサポートされているケースは限られます。もしそのような高度な計算が必要な場合は、GoからC/C++などで書かれたハードウェアアクセラレーションを利用するライブラリを呼び出すなどの連携が必要になるでしょう。

どの方法を選ぶべきか?

どの代替方法を選ぶかは、以下の要素によって決まります。

  • プロジェクトの規模と複雑さ: システム全体の設計に与える影響。
  • 依存関係: 外部ライブラリへの依存をどこまで許容できるか。
  • 開発の容易さ: ライブラリの使いやすさやドキュメントの充実度。
  • パフォーマンス要件: 計算速度がどの程度重要か。
  • 必要な精度: どの程度の精度が求められるか。

一般的には、Goの標準ライブラリである "math/big" が多くのケースで十分に機能し、最も手軽に利用できる選択肢です。しかし、特定の要件がある場合には、上記の代替方法も検討する価値があります。