Go言語 big.Int.Mul()徹底解説:巨大整数計算の基本と応用

2025-06-01

big.Int.Mul()の基本的な使い方

Mulメソッドのシグネチャは以下のようになっています。

func (z *Int) Mul(x, y *Int) *Int

これは、次のように解釈できます。

  • x, y *Int: 掛け算を行う2つのbig.Int型のポインタです。xyを掛け合わせます。
  • z *Int: 演算結果を格納するbig.Int型のポインタです。Mulメソッドは、xyの積を計算し、その結果をzに代入します。そして、そのz自身を返します。

重要なポイント

  1. 結果はレシーバに格納される
    Mulメソッドは、結果をレシーバ(この場合はz)に直接書き込みます。そのため、zは結果を格納するためのbig.Int型の変数である必要があります。
  2. ポインタで渡す
    big.Int型の値を扱う際は、常にポインタ(*big.Int)で渡す必要があります。これは、Goの標準の整数型とは異なる点です。
  3. 通常の演算子(*)は使えない
    big.Int型に対しては、通常の*演算子(アスタリスク)を使った掛け算はできません。必ずMulメソッドを使用する必要があります。

具体例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// big.Int の初期化
	a := big.NewInt(1234567890123456789) // 非常に大きな整数
	b := big.NewInt(9876543210987654321) // 別の非常に大きな整数

	// 結果を格納するための big.Int を用意
	result := new(big.Int)

	// a と b の積を計算し、result に格納
	result.Mul(a, b)

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

	// 別な例:同じ big.Int を再利用して計算
	c := big.NewInt(5)
	d := big.NewInt(10)
	e := big.NewInt(20)

	// c = c * d (cの値が上書きされる)
	c.Mul(c, d) 
	fmt.Printf("c (5*10) = %s\n", c.String()) // 結果: 50

	// c = c * e (cの値が再度上書きされる)
	c.Mul(c, e) 
	fmt.Printf("c (50*20) = %s\n", c.String()) // 結果: 1000
}

Goの組み込みの整数型(int, int64など)は、固定のビット数で表現されるため、扱える数値の範囲に制限があります。例えば、int64型は最大で約9×1018までしか表現できません。

しかし、暗号通貨、科学技術計算、非常に大きな数を扱うアルゴリズムなど、この範囲を超える巨大な整数を扱う必要がある場合があります。そのような場合に、math/bigパッケージが提供するbig.Int型が役立ちます。big.Intは、メモリが許す限り任意の精度の整数を表現できます。



nilポインタのデアファレンス (nil dereference)

これは最も頻繁に発生するエラーの一つです。big.Int型の変数を初期化せずにMulメソッドを呼び出そうとすると発生します。

エラーの例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	var a *big.Int // 初期化されていない (nil)
	b := big.NewInt(10)
	c := big.NewInt(5)

	// ここでパニックが発生する可能性が高い (a は nil なので)
	a.Mul(b, c) // runtime error: invalid memory address or nil pointer dereference

	fmt.Printf("Result: %s\n", a.String())
}

原因
big.Int型はポインタとして扱われるため、使用する前にnew(big.Int)またはbig.NewInt()などで初期化する必要があります。var a *big.Intとしただけでは、anilポインタのままです。

トラブルシューティング/解決策

結果を格納するbig.Intも、引数として渡すbig.Intも、必ず使用前に初期化します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 解決策1: new(big.Int) で初期化
	var a = new(big.Int) // 結果を格納するための big.Int を初期化

	b := big.NewInt(10)
	c := big.NewInt(5)

	a.Mul(b, c) // 正しく動作する
	fmt.Printf("Result: %s\n", a.String()) // Result: 50

	// 解決策2: big.NewInt() で初期化 (初期値も設定できる)
	d := big.NewInt(0) // または big.NewInt(10) など
	e := big.NewInt(20)
	f := big.NewInt(3)

	d.Mul(e, f) // 正しく動作する
	fmt.Printf("Result: %s\n", d.String()) // Result: 60
}

結果のレシーバ(z)と引数(x, y)が同じインスタンスの場合

Mulメソッドは結果をレシーバに上書きします。この性質を理解していないと、意図しない結果になることがあります。

問題の例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewInt(10)
	y := big.NewInt(5)

	// x = x * y を意図しているが、結果が異なる可能性がある
	// 実際には y の値が計算中に変更されてしまう可能性がある
	x.Mul(x, y) // 問題ではないが、意図しない結果になることがある

	fmt.Printf("Result: %s\n", x.String())
}

この例自体は動作しますが、より複雑な計算式で同じbig.Intインスタンスを複数の操作で使い回す場合に、予期せぬ中間結果の上書きにより最終結果がおかしくなることがあります。特に、big.Int.Mul(x, x)のような自己乗算を行う場合や、複数の操作をチェインする場合に注意が必要です。

トラブルシューティング/解決策

Mulメソッドは、レシーバと引数が同じbig.Intインスタンスであっても正しく動作するように設計されています(つまり、x.Mul(x, y)x.Mul(y, x)x.Mul(x, x)は安全です)。

ただし、読みやすさや意図の明確化のため、中間結果を別のbig.Int変数に格納することをお勧めします。特に、複雑な式を評価する際には、結果を格納するための新しいbig.Intインスタンスを準備するのが安全です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	x := big.NewInt(10)
	y := big.NewInt(5)
	z := big.NewInt(2)

	// (x * y) + z を計算したい場合
	tempResult := new(big.Int) // 中間結果を格納するための新しい big.Int
	tempResult.Mul(x, y)        // tempResult = x * y

	finalResult := new(big.Int)
	finalResult.Add(tempResult, z) // finalResult = tempResult + z

	fmt.Printf("Result: %s\n", finalResult.String()) // Result: 52
}

通常の演算子 (*) の使用

big.Int型に対しては、Goの組み込み型のように*演算子を使って直接掛け算することはできません。

エラーの例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(10)
	b := big.NewInt(5)

	// コンパイルエラー: invalid operation: a * b (operator * not defined on big.Int)
	// result := a * b

	// コンパイルエラー: invalid operation: a * 5 (mismatched types *big.Int and int)
	// result := a * 5
}

原因
Goの演算子はオーバーロードできないため、big.Intのようなカスタム型に対しては、専用のメソッド(この場合はMul())を使用する必要があります。

トラブルシューティング/解決策
常にbig.Int.Mul()メソッドを使用します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(10)
	b := big.NewInt(5)

	result := new(big.Int)
	result.Mul(a, b) // 正しい使い方

	fmt.Printf("Result: %s\n", result.String()) // Result: 50
}

型変換の不足

big.Intは他の数値型(int64float64など)と直接演算できません。必要に応じて型変換を行う必要があります。

問題の例

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(10)
	b := 5 // int 型

	// コンパイルエラー: mismatched types *big.Int and int
	// result.Mul(a, b)

	// ここでも型変換が必要
	// result := new(big.Int)
	// result.Add(a, b) // Add も同様に直接は使えない
}

トラブルシューティング/解決策
big.Int型と他の型の間で演算を行う場合、big.Int.SetInt64()big.NewInt()などを使用してbig.Int型に変換してから演算を行います。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(10)
	b := 5        // int 型
	c := int64(2) // int64 型

	// int を big.Int に変換してから Mul
	bigB := big.NewInt(int64(b)) // int を int64 にキャストしてから big.NewInt
	result1 := new(big.Int)
	result1.Mul(a, bigB)
	fmt.Printf("a * b: %s\n", result1.String()) // a * b: 50

	// int64 を big.Int に変換してから Mul
	bigC := big.NewInt(c)
	result2 := new(big.Int)
	result2.Mul(a, bigC)
	fmt.Printf("a * c: %s\n", result2.String()) // a * c: 20
}

big.Int.Mul()を使用する際の主なトラブルシューティングのポイントは以下の通りです。

  • 他の数値型と演算する際は、必ずbig.Int型に変換すること。
  • 通常の*演算子はbig.Int型には使えないので、常にMul()メソッドを使用すること。
  • 結果を格納するbig.Intと引数のbig.Intが同じインスタンスでも動作はするが、複雑な計算では中間結果用に新しいbig.Intインスタンスを用意するのが安全で分かりやすい。
  • nilポインタの初期化忘れに注意し、new(big.Int)big.NewInt()で必ず初期化すること。


基本的な掛け算の例

これは最も基本的な使い方です。2つのbig.Int型の値を掛け合わせ、その結果を別のbig.Int変数に格納します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	// 1. big.Int の初期化
	// big.NewInt(値) で初期化するのが最も一般的です。
	// ここでは、int64の最大値を超えるような大きな数値を設定してみます。
	num1 := big.NewInt(0)
	num1.SetString("123456789012345678901234567890", 10) // 10進数で文字列から設定

	num2 := big.NewInt(0)
	num2.SetString("987654321098765432109876543210", 10) // 別の大きな数値

	// 2. 結果を格納するための big.Int 変数を準備
	// new(big.Int) でゼロ値の big.Int ポインタを作成します。
	result := new(big.Int)

	// 3. Mul メソッドを使って掛け算を実行
	// result.Mul(num1, num2) は、num1 と num2 の積を計算し、
	// その結果を result に格納します。
	result.Mul(num1, num2)

	// 4. 結果の出力
	// big.Int の値を文字列として出力するには .String() メソッドを使います。
	fmt.Printf("num1 = %s\n", num1.String())
	fmt.Printf("num2 = %s\n", num2.String())
	fmt.Printf("num1 * num2 = %s\n", result.String())

	// 出力例:
	// num1 = 123456789012345678901234567890
	// num2 = 987654321098765432109876543210
	// num1 * num2 = 12193263111263526913617349141042784518335212389146197305910900
}

解説

  • Mul(x, y *Int): xyを掛け、その結果をレシーバ(この場合はresult)に格納します。
  • new(big.Int): 結果を格納するbig.Intのポインタを初期化するために使用します。nilポインタの参照を防ぐために、必ずMulメソッドを呼び出す前に初期化してください。
  • SetString(s string, base int): big.Intは、int64の範囲を超える数値を直接リテラルで書くことができないため、文字列として与え、基数(この場合は10進数)を指定して設定します。

同じ変数での上書き(自己参照)の例

Mulメソッドは、結果をレシーバに上書きします。引数とレシーバが同じbig.Intインスタンスであっても正しく動作します。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	val := big.NewInt(10) // 初期値 10

	fmt.Printf("Initial val = %s\n", val.String()) // Initial val = 10

	// val = val * 5
	val.Mul(val, big.NewInt(5))
	fmt.Printf("val * 5 = %s\n", val.String()) // val * 5 = 50

	// val = val * val (val の自乗)
	val.Mul(val, val)
	fmt.Printf("val * val (50 * 50) = %s\n", val.String()) // val * val (50 * 50) = 2500

	// 複数の掛け算をチェインすることも可能
	// val = val * 2 * 3
	val.Mul(val, big.NewInt(2)).Mul(val, big.NewInt(3))
	fmt.Printf("val * 2 * 3 (2500 * 2 * 3) = %s\n", val.String()) // val * 2 * 3 (2500 * 2 * 3) = 15000
}

解説

  • メソッドチェーン: Mulメソッドは*big.Intを返すため、.Mul(...)のように続けて別のMulメソッドを呼び出すことができます。これは便利ですが、計算の順序と結果の格納先を明確に理解しておく必要があります。
  • val.Mul(val, val): valの自乗を計算し、valに格納します。
  • val.Mul(val, big.NewInt(5)): valの現在の値に5を掛け、その結果を再びvalに格納します。元のvalの値は上書きされます。

他の型との掛け算(型変換の必要性)の例

big.IntはGoの組み込み型(int, int64など)と直接演算できません。他の型をbig.Intに変換してからMulメソッドを使う必要があります。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	bigNum := big.NewInt(100) // big.Int 型

	intNum := 5       // int 型
	int64Num := int64(20) // int64 型

	// intNum を big.Int に変換してから掛け算
	result1 := new(big.Int)
	result1.Mul(bigNum, big.NewInt(int64(intNum))) // int を int64 にキャストしてから big.NewInt
	fmt.Printf("bigNum * intNum = %s\n", result1.String()) // bigNum * intNum = 500

	// int64Num を big.Int に変換してから掛け算
	result2 := new(big.Int)
	result2.Mul(bigNum, big.NewInt(int64Num))
	fmt.Printf("bigNum * int64Num = %s\n", result2.String()) // bigNum * int64Num = 2000

	// 負の数との掛け算
	negativeNum := big.NewInt(-7)
	result3 := new(big.Int)
	result3.Mul(bigNum, negativeNum)
	fmt.Printf("bigNum * negativeNum = %s\n", result3.String()) // bigNum * negativeNum = -700
}

解説

  • big.NewInt(int64(intNum)): int型のintNumを直接big.NewInt()に渡すことはできません(型が異なるため)。一度int64にキャストしてからbig.NewInt()に渡すことで、big.Int型の値を作成しています。

複数のbig.Intの演算を組み合わせて複雑な式を計算する例です。中間結果を格納するbig.Int変数を適宜用意することが重要です。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	A := big.NewInt(0)
	A.SetString("1000000000000000000", 10) // 10^18

	B := big.NewInt(0)
	B.SetString("2000000000000000000", 10) // 2 * 10^18

	C := big.NewInt(5)
	D := big.NewInt(3)

	// 例: (A * B) - (C * D) を計算する
	// まず A * B を計算
	mulAB := new(big.Int)
	mulAB.Mul(A, B)
	fmt.Printf("A * B = %s\n", mulAB.String())

	// 次に C * D を計算
	mulCD := new(big.Int)
	mulCD.Mul(C, D)
	fmt.Printf("C * D = %s\n", mulCD.String())

	// 最後に引き算 (math/big パッケージの Add/Sub なども同様のインターフェース)
	finalResult := new(big.Int)
	finalResult.Sub(mulAB, mulCD) // Subtract メソッド

	fmt.Printf("(A * B) - (C * D) = %s\n", finalResult.String())

	// 出力例:
	// A * B = 2000000000000000000000000000000000000
	// C * D = 15
	// (A * B) - (C * D) = 1999999999999999999999999999999999985
}
  • math/bigパッケージには、Mulだけでなく、Add(足し算)、Sub(引き算)、Div(割り算)、Mod(剰余)、Exp(べき乗)など、さまざまな算術演算メソッドが用意されており、これらも同様のインターフェース(z.Op(x, y)形式)を持っています。
  • mulABmulCDという中間結果を格納するためのbig.Int変数を導入しています。これにより、計算の各ステップが明確になり、コードの読みやすさとデバッグのしやすさが向上します。


厳密に言えば、Go言語の標準ライブラリにおいて、big.Int型の値を「掛け算」するための直接的な「代替メソッド」は存在しません。math/bigパッケージが提供するMul()メソッドが、その目的のための唯一の公式な方法です。

しかし、「代替手段」という言葉を広くとらえ、以下のような観点から解説します。

  1. 他の演算との組み合わせによる乗算のシミュレーション(非現実的)
  2. パフォーマンスが問題になる場合の最適化戦略
  3. サードパーティライブラリの利用(一般的ではないが可能性として)
  4. そもそもbig.Intが必要ないケース

他の演算との組み合わせによる乗算のシミュレーション (非現実的)

これは理論上の話であり、実際のプログラミングでMul()の代替として使うことは全く推奨されません。多倍長整数の掛け算は、コンピュータ内部では基本的に「繰り返し足し算」や、より効率的なアルゴリズム(例: Karatsubaアルゴリズム、Toom-Cookアルゴリズムなど)で実装されています。

例えば、A×B は、A を B 回足すことと同じです。

package main

import (
	"fmt"
	"math/big"
)

func main() {
	a := big.NewInt(100)
	b := big.NewInt(5)

	// Mul の代替として、a を b 回足す (非常に非効率!)
	simulatedResult := big.NewInt(0) // 初期値 0

	// b の値を int64 に変換(b が大きすぎるとオーバーフローする)
	bInt64 := b.Int64()
	if b.Cmp(big.NewInt(0)) < 0 { // b が負の場合
		fmt.Println("Negative multiplier is not supported in this simulation.")
		return
	}
	if b.Cmp(big.NewInt(1<<63-1)) > 0 { // b が int64 の最大値を超える場合
		fmt.Println("Multiplier is too large for int64 conversion.")
		return
	}

	for i := int64(0); i < bInt64; i++ {
		simulatedResult.Add(simulatedResult, a) // simulatedResult = simulatedResult + a
	}

	fmt.Printf("a = %s, b = %s\n", a.String(), b.String())
	fmt.Printf("Simulated a * b = %s\n", simulatedResult.String()) // 結果: 500

	// 比較のための正規の Mul
	actualResult := new(big.Int).Mul(a, b)
	fmt.Printf("Actual a * b = %s\n", actualResult.String()) // 結果: 500
}

なぜ非現実的か

  • int64への変換が必要
    ループのカウンタは通常の整数型で管理する必要があるため、掛け算の片方がint64の範囲に収まらない場合、この方法自体が破綻します。
  • 非常に非効率的
    big.Intは巨大な数を扱うため、Bが非常に大きい場合(例: 10100)、このループは天文学的な回数実行され、現実的な時間では完了しません。Mul()メソッドは、このような大きな数の掛け算に特化した高速なアルゴリズムを内部で利用しています。

したがって、これはあくまで概念的な説明であり、実際のコードでMul()の代わりに使うべきではありません。

パフォーマンスが問題になる場合の最適化戦略

big.Int.Mul()自体が非常に最適化されたコードですが、アプリケーション全体のパフォーマンスが問題になる場合、Mul()の呼び出し回数を減らす、またはbig.Int自体の利用を避けるなどのアプローチが「代替」となり得ます。

  • 並列化

    • 複数の独立したbig.Int.Mul()演算がある場合、Goのgoroutineとチャネルを使って並列に実行することで、全体のスループットを向上させることが可能です。ただし、一つのMul()演算自体を並列化するのは、math/bigパッケージの内部実装に依存するため、通常はできません。
  • 計算結果のキャッシュ

    • 同じbig.Int.Mul()の結果が何度も必要になる場合、一度計算した結果をキャッシュ(メモリに保存)しておき、再利用することで計算時間を短縮します。特に、非常に大きな数の掛け算は計算コストが高いため、キャッシュは有効な戦略です。
  • 必要最小限のbig.Intの利用

    • 計算の途中でbig.Intが必要になる部分だけをbig.Intにし、それ以外の部分は通常のint64などで処理できないか検討します。
    • 例えば、小さな定数との乗算であれば、big.NewInt(someInt64).Mul(someBigInt, big.NewInt(smallInt64))のように、都度big.Intに変換して計算します。
    • 計算ロジックを見直し、big.Intでの乗算回数を減らせないか検討します。例えば、共通因数を事前に計算しておく、指数計算の場合はExp()メソッド(べき乗)を利用するなどです。
    • big.Int.Exp(x, y, m)は、xy(modm) を効率的に計算できます。もしモジュロ演算が必要な場合、これは繰り返し乗算よりもはるかに高速です。

サードパーティライブラリの利用 (一般的ではないが可能性として)

Goのmath/bigパッケージは多倍長整数演算の標準実装であり、非常に効率的です。しかし、ごく稀に、特定のニッチな要件(例: 特定のハードウェアに最適化された実装、異なるアルゴリズムの評価など)のために、サードパーティの多倍長整数ライブラリが存在する可能性があります。

そもそもbig.Intが必要ないケース

これは「代替手段」というよりは、「big.Int.Mul()を使うべきではないケース」と考えるべきです。

  • 浮動小数点数の計算が必要な場合
    もし計算結果が整数ではなく、非常に高精度の浮動小数点数が必要な場合は、math/big.Float型を検討するべきです。big.Intは整数のみを扱います。

  • int64の範囲に収まる場合
    計算結果がint64(約9×1018)の範囲に収まることが確実であれば、通常のint64型を使用するべきです。big.Intを使うと、アロケーション(メモリ確保)やポインタのデリファレンス、内部の複雑なアルゴリズムのため、通常のint64演算に比べて著しくパフォーマンスが低下します

    package main
    
    import "fmt"
    
    func main() {
    	// big.Int を使う必要がない例
    	a := 123456789
    	b := 987654321
    
    	// 結果が int64 の範囲に収まることが明らかな場合
    	result := int64(a) * int64(b) // int を int64 にキャストして乗算
    	fmt.Printf("int64 result = %d\n", result) // int64 result = 121932631112635269
    }
    

Go言語でbig.Int型の掛け算を行う場合、big.Int.Mul()メソッドが標準的かつ最も効率的な唯一の公式な方法です。

「代替手段」を考える場合は、以下の点を考慮してください。

  • そもそもbig.Intが必要なのかを再検討し、int64float64で十分な場合はそちらを使用するべきです。
  • パフォーマンスが問題の場合、Mul()自体の代替ではなく、計算アルゴリズムの改善キャッシュ並列化big.Intの利用範囲の限定といったより上位の最適化戦略を検討します。
  • Mul()を他の演算でシミュレートするのは非現実的で非効率的です。