big.Intの例で学ぶGo言語:階乗、べき乗、比較演算

2025-06-01

big.Int は、Goの標準パッケージである math/big パッケージで提供されている型の一つです。これは、標準の整数型 (int, int64 など) の範囲を超える非常に大きな整数を扱うために使用されます。

標準の整数型には、扱える値の範囲に上限と下限があります。たとえば、int64 型の場合、通常は約 −263 から 263−1 までの整数しか表現できません。しかし、big.Int を使うと、これらの制限を超えた、事実上無限の大きさの整数を扱うことができるようになります。

big.Int の主な特徴と用途

  • 暗号処理や数学的な計算
    非常に大きな数を扱う必要のある暗号処理 (RSAなど) や、高度な数学的な計算でよく利用されます。
  • 文字列との変換
    大きな整数を文字列として表現したり (String)、文字列から big.Int 型の変数に変換したり (SetString) することができます。
  • 比較演算のサポート
    等しい (Cmp が 0 を返す)、より大きい (Cmp が 1 を返す)、より小さい (Cmp が -1 を返す) などの比較を行うことができます。
  • ビット演算のサポート
    AND (And)、OR (Or)、XOR (Xor)、NOT (Not)、ビットシフト (Lsh, Rsh) などのビット演算もサポートされています。
  • 算術演算のサポート
    加算 (Add)、減算 (Sub)、乗算 (Mul)、除算 (Div)、剰余 (Mod)、べき乗 (Exp) など、基本的な算術演算を行うためのメソッドが提供されています。
  • 非常に大きな整数の表現
    桁数が数百、数千、あるいはそれ以上の整数でも正確に表現し、計算することができます。

基本的な使い方

big.Int 型の変数を使用するには、まず math/big パッケージをインポートする必要があります。

import "math/big"

そして、big.Int 型の変数を宣言し、必要に応じて値を設定します。値を設定する一般的な方法としては、SetInt64 メソッドや SetString メソッドがあります。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 新しい big.Int 変数の作成
	a := new(big.Int)
	b := new(big.Int)
	result := new(big.Int)

	// int64 型の値から設定
	a.SetInt64(1234567890)
	b.SetInt64(9876543210)

	// 文字列から設定
	c := new(big.Int)
	c.SetString("123456789012345678901234567890", 10) // 10進数として解釈

	// 算術演算の例
	result.Add(a, b)
	fmt.Printf("a + b = %s\n", result.String())

	result.Mul(a, c)
	fmt.Printf("a * c = %s\n", result.String())

	// 比較演算の例
	if a.Cmp(b) > 0 {
		fmt.Println("a は b より大きい")
	} else if a.Cmp(b) < 0 {
		fmt.Println("a は b より小さい")
	} else {
		fmt.Println("a と b は等しい")
	}

	fmt.Printf("c = %s\n", c.String())
}
  • big.Int の演算は、標準の整数型に比べて処理に時間がかかる場合があります。パフォーマンスが重要な場面では、可能な限り標準の整数型を使用することを検討する必要があります。
  • big.Int 同士の演算は、メソッドを呼び出して行います。直接 +, -, *, / などの演算子を使用することはできません。
  • big.Int 型の変数は、ポインタ型 (*big.Int) であることが多いです。これは、内部的に大きなデータを効率的に扱うためです。


一般的なエラーとトラブルシューティング

    • エラー
      big.Int 型の変数はポインタ型 (*big.Int) であるため、初期化せずにそのままメソッドを呼び出そうとすると、panic: runtime error: invalid memory address or nil pointer dereference が発生します。
    • 原因
      new(big.Int)big.NewInt(value) などで明示的に big.Int のインスタンスを作成し、そのポインタを変数に代入する必要があります。単に var myInt *big.Int と宣言しただけでは、myIntnil のままです。
    • 解決策
      var myInt *big.Int
      myInt = new(big.Int) // または myInt = big.NewInt(0) などで初期化
      myInt.SetInt64(123)   // 初期化後にメソッドを呼び出す
      
  1. 演算子 (+, -, *, /) の直接使用

    • エラー
      標準の整数型とは異なり、big.Int 型の変数に対して直接算術演算子を使用することはできません。コンパイルエラーが発生します。
    • 原因
      big.Int は内部的に大きな数を効率的に扱うための構造を持っているため、専用のメソッド (Add, Sub, Mul, Div など) を使って演算を行う必要があります。
    • 解決策
      a := big.NewInt(10)
      b := big.NewInt(5)
      result := new(big.Int)
      
      result.Add(a, b) // 加算
      fmt.Println(result)
      
      result.Sub(a, b) // 減算
      fmt.Println(result)
      
      result.Mul(a, b) // 乗算
      fmt.Println(result)
      
      result.Div(a, b) // 除算
      fmt.Println(result)
      
  2. SetString の誤った基数 (Incorrect base for SetString)

    • エラー
      SetString(s string, base int) メソッドで文字列から big.Int に変換する際、指定した基数 (base) が実際の文字列の形式と異なると、正しく変換されません。基数が 0 の場合は、プレフィックス ("0b", "0", "0x") に基づいて自動的に判別されますが、予期しない形式の文字列を渡すとエラーになる可能性があります。
    • 原因
      文字列が指定された基数で表現されていない。例えば、2進数の文字列なのに基数を 10 で指定したりする場合など。
    • 解決策
      SetString に渡す文字列の形式と一致する正しい基数を指定するか、基数を 0 にして自動判別に任せる場合は、文字列が "0b", "0", "0x" のいずれかのプレフィックスで始まるようにします。変換が成功したかどうかは、SetString の戻り値 (成功したかどうかを示す bool) を確認することも重要です。
  3. Div および Mod のゼロ除算 (Division by zero)

    • エラー
      Div (除算) や Mod (剰余) の操作で、除数がゼロの big.Int である場合に、panic: runtime error: division by zero が発生します。
    • 原因
      数学的なルールとして、ゼロで除算することはできません。
    • 解決策
      除算や剰余演算を行う前に、除数がゼロでないことを確認する必要があります。Cmp(big.NewInt(0)) を使って除数がゼロかどうかを比較できます。
  4. String() メソッドの理解不足

    • 誤解
      big.Int 型の変数を直接 fmt.Println などで出力しようとして、期待通りの数値の文字列が表示されないと感じる場合があります。
    • 原因
      big.Int 型の変数を直接出力すると、内部構造に関する情報が出力される可能性があります。
    • 解決策
      big.Int の値を文字列として取得するには、必ず String() メソッドを使用します。
      num := big.NewInt(12345)
      fmt.Println(num.String()) // "12345" と出力される
      
  5. 大きな数によるパフォーマンスの問題

    • 問題
      big.Int は非常に大きな数を扱えますが、その演算は標準の整数型に比べて計算コストが高くなる可能性があります。特に、非常に大きな数の繰り返し演算を行う場合、パフォーマンスに影響が出ることがあります。
    • 原因
      内部的に複雑な処理を行って大きな数を扱っているため。
    • 解決策
      パフォーマンスが重要な場面では、本当に big.Int が必要かどうかを検討し、可能な限り標準の整数型を使用することを検討します。アルゴリズムを見直して、big.Int の使用頻度を減らすことも有効かもしれません。
  6. 符号の扱い

    • 注意点
      big.Int は符号付きの整数を扱います。演算を行う際には、符号がどのように影響するかを理解しておく必要があります。例えば、負の数同士の乗算は正の数になります。

トラブルシューティングのヒント

  • ログ出力を活用する
    中間結果をログ出力することで、変数の状態や処理の流れを確認できます。
  • ドキュメントを参照する
    math/big パッケージの公式ドキュメントには、各メソッドの詳細な説明や使用例が記載されています。
  • 簡単な例で試す
    問題が複雑な場合に、小さな値や簡単な演算で動作を確認してみることで、問題の切り分けができます。
  • エラーメッセージをよく読む
    Goのエラーメッセージは、問題の原因を特定する上で非常に役立ちます。


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

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) // 10進数として設定
	if !ok {
		fmt.Println("num2 の設定に失敗しました")
		return
	}

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

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

この例では、文字列で表現された非常に大きな2つの整数を big.Int 型の変数に変換し、それらの和と積を計算しています。SetString 関数を使って文字列を big.Int に変換する際に、基数 (ここでは 10進数) を指定している点に注意してください。

例2: 階乗の計算

package main

import (
	"fmt"
	"math/big"
)

// n の階乗を計算する関数
func factorial(n int64) *big.Int {
	result := big.NewInt(1)
	for i := int64(2); 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())
}

この例では、big.Int を使って大きな数の階乗を計算しています。標準の整数型ではすぐにオーバーフローしてしまうような大きな n に対しても、正確な階乗を計算できます。ループの中で Mul メソッドを使って累積的に乗算を行っています。

例3: べき乗の計算

package main

import (
	"fmt"
	"math/big"
)

func main() {
	base := big.NewInt(2)
	exponent := big.NewInt(100)
	modulus := big.NewInt(1000000007) // 例として大きな素数

	// base の exponent 乗を計算
	power := new(big.Int)
	power.Exp(base, exponent, nil) // 剰余なし

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

	// base の exponent 乗を modulus で割った余りを計算
	modPower := new(big.Int)
	modPower.Exp(base, exponent, modulus)

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

この例では、Exp メソッドを使ってべき乗の計算を行っています。3つ目の引数に nil を渡すと、単純なべき乗が計算されます。3つ目の引数に big.Int 型の値を渡すと、べき乗の結果をその値で割った剰余が計算されます。これは、暗号処理などでよく使われる操作です。

例4: 比較演算

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(1000)
	b := big.NewInt(2000)
	c := big.NewInt(1000)

	fmt.Printf("a = %s, b = %s, c = %s\n", a.String(), b.String(), c.String())

	if a.Cmp(b) < 0 {
		fmt.Println("a は b より小さい")
	}

	if a.Cmp(c) == 0 {
		fmt.Println("a と c は等しい")
	}

	if b.Cmp(a) > 0 {
		fmt.Println("b は a より大きい")
	}
}

この例では、Cmp メソッドを使って big.Int 同士の比較を行っています。Cmp メソッドは、レシーバー (a) が引数 (b) より小さい場合は -1、等しい場合は 0、大きい場合は 1 を返します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(10) // 二進数: 1010
	b := big.NewInt(5)  // 二進数: 0101

	andResult := new(big.Int)
	andResult.And(a, b) // AND演算

	orResult := new(big.Int)
	orResult.Or(a, b)   // OR演算

	xorResult := new(big.Int)
	xorResult.Xor(a, b) // XOR演算

	notResult := new(big.Int).Not(a) // NOT演算 (注意: これは表現できる最大の数とのXORになります)

	lshResult := new(big.Int).Lsh(a, 2) // 左シフト

	rshResult := new(big.Int).Rsh(a, 1) // 右シフト

	fmt.Printf("a   = %s (binary: %b)\n", a.String(), a)
	fmt.Printf("b   = %s (binary: %b)\n", b.String(), b)
	fmt.Printf("a & b = %s (binary: %b)\n", andResult.String(), andResult)
	fmt.Printf("a | b = %s (binary: %b)\n", orResult.String(), orResult)
	fmt.Printf("a ^ b = %s (binary: %b)\n", xorResult.String(), xorResult)
	fmt.Printf("^a  = %s (binary: %b)\n", notResult.String(), notResult)
	fmt.Printf("a << 2 = %s (binary: %b)\n", lshResult.String(), lshResult)
	fmt.Printf("a >> 1 = %s (binary: %b)\n", rshResult.String(), rshResult)
}


標準の整数型 (int, int64 など) の利用

  • 注意点
    オーバーフローのリスクがあるため、計算結果が範囲を超える可能性がある場合は注意が必要です。
  • 用途
    扱える数値の範囲が事前に分かっており、それが標準の整数型の範囲内に収まる場合。パフォーマンスが重要な処理。
  • 説明
    扱える整数の範囲に制限があるものの、標準の整数型は big.Int よりも高速に演算を行うことができます。多くの一般的な計算では、標準の整数型で十分な場合があります。

浮動小数点数型 (float64 など) の利用

  • 注意点
    整数としての正確な値は保証されません。金融計算や正確なカウントが必要な場合には不向きです。
  • 用途
    科学技術計算など、おおよその magnitude (桁数) を扱えれば良い場合。
  • 説明
    非常に大きな数値を指数形式で近似的に表現できます。ただし、精度が失われる可能性があります。整数としての正確な値を保持する必要がない場合に限られます。

外部ライブラリの利用

  • 注意点
    外部ライブラリの導入と管理が必要です。ライブラリの品質やメンテナンス状況も考慮する必要があります。
  • 用途
    高精度な10進数演算、特定の暗号処理など、標準の big.Int では機能が不足する場合や、より特化した機能が必要な場合。

    • github.com/shopspring/decimal
      高精度な10進数を扱うためのライブラリです。金融計算など、浮動小数点数の誤差が許容されない場合に適しています。内部的には大きな整数として数値を保持し、演算を行います。
    • 他の暗号関連ライブラリ
      特定の暗号アルゴリズムに最適化された大きな整数演算機能を提供している場合があります。
  • 説明
    Goのエコシステムには、big.Int 以外にも大きな数を扱うためのライブラリが存在します。これらのライブラリは、特定の用途に特化していたり、big.Int にはない機能を提供したりする場合があります。

文字列としての数値の扱い

  • 注意点
    演算の実装が複雑になる可能性があり、パフォーマンスも一般的に big.Int より劣ります。
  • 用途
    極めて特殊な数値演算、他のシステムとの連携で文字列形式での数値交換が必要な場合。
  • 説明
    非常に大きな数値を文字列として保持し、必要な演算を自力で実装したり、他の言語のライブラリを呼び出したりする方法です。Goだけで完結しない場合や、特殊な演算が必要な場合に検討されます。

ハードウェアアクセラレーションや外部サービス

  • 注意点
    ハードウェアや外部サービスの導入、利用コストが発生します。ネットワーク経由の通信遅延も考慮する必要があります。
  • 用途
    極めて高いパフォーマンスが要求される場合。
  • 説明
    特定の非常に負荷の高い大きな整数の演算 (例えば、暗号処理の一部) を、専用のハードウェアや外部のクラウドサービスにオフロードする方法です。

big.Int を選択する主な理由

  • 標準ライブラリの一部
    追加の依存関係なしに利用できます。
  • 豊富な算術演算とビット演算
    加減乗除、剰余、べき乗、ビット操作など、多くの基本的な演算が標準でサポートされています。
  • 無限の精度
    標準の整数型の範囲を超える非常に大きな整数を正確に扱えます。
  • 特定の機能
    big.Int にはない特殊な演算やデータ型が必要な場合があります。
  • 精度
    浮動小数点数は近似的な表現であり、誤差が生じる可能性があります。高精度な10進数演算には専用のライブラリが適しています。
  • パフォーマンス
    標準の整数型の方が一般的に高速です。