big.Intの例で学ぶGo言語:階乗、べき乗、比較演算
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
と宣言しただけでは、myInt
はnil
のままです。 - 解決策
var myInt *big.Int myInt = new(big.Int) // または myInt = big.NewInt(0) などで初期化 myInt.SetInt64(123) // 初期化後にメソッドを呼び出す
- エラー
-
演算子 (+, -, *, /) の直接使用
- エラー
標準の整数型とは異なり、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)
- エラー
-
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
) を確認することも重要です。
- エラー
-
Div および Mod のゼロ除算 (Division by zero)
- エラー
Div
(除算) やMod
(剰余) の操作で、除数がゼロのbig.Int
である場合に、panic: runtime error: division by zero が発生します。 - 原因
数学的なルールとして、ゼロで除算することはできません。 - 解決策
除算や剰余演算を行う前に、除数がゼロでないことを確認する必要があります。Cmp(big.NewInt(0))
を使って除数がゼロかどうかを比較できます。
- エラー
-
String() メソッドの理解不足
- 誤解
big.Int
型の変数を直接fmt.Println
などで出力しようとして、期待通りの数値の文字列が表示されないと感じる場合があります。 - 原因
big.Int
型の変数を直接出力すると、内部構造に関する情報が出力される可能性があります。 - 解決策
big.Int
の値を文字列として取得するには、必ずString()
メソッドを使用します。num := big.NewInt(12345) fmt.Println(num.String()) // "12345" と出力される
- 誤解
-
大きな数によるパフォーマンスの問題
- 問題
big.Int
は非常に大きな数を扱えますが、その演算は標準の整数型に比べて計算コストが高くなる可能性があります。特に、非常に大きな数の繰り返し演算を行う場合、パフォーマンスに影響が出ることがあります。 - 原因
内部的に複雑な処理を行って大きな数を扱っているため。 - 解決策
パフォーマンスが重要な場面では、本当にbig.Int
が必要かどうかを検討し、可能な限り標準の整数型を使用することを検討します。アルゴリズムを見直して、big.Int
の使用頻度を減らすことも有効かもしれません。
- 問題
-
符号の扱い
- 注意点
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進数を扱うためのライブラリです。金融計算など、浮動小数点数の誤差が許容されない場合に適しています。内部的には大きな整数として数値を保持し、演算を行います。 - 他の暗号関連ライブラリ
特定の暗号アルゴリズムに最適化された大きな整数演算機能を提供している場合があります。
- github.com/shopspring/decimal
- 説明
Goのエコシステムには、big.Int
以外にも大きな数を扱うためのライブラリが存在します。これらのライブラリは、特定の用途に特化していたり、big.Int
にはない機能を提供したりする場合があります。
文字列としての数値の扱い
- 注意点
演算の実装が複雑になる可能性があり、パフォーマンスも一般的にbig.Int
より劣ります。 - 用途
極めて特殊な数値演算、他のシステムとの連携で文字列形式での数値交換が必要な場合。 - 説明
非常に大きな数値を文字列として保持し、必要な演算を自力で実装したり、他の言語のライブラリを呼び出したりする方法です。Goだけで完結しない場合や、特殊な演算が必要な場合に検討されます。
ハードウェアアクセラレーションや外部サービス
- 注意点
ハードウェアや外部サービスの導入、利用コストが発生します。ネットワーク経由の通信遅延も考慮する必要があります。 - 用途
極めて高いパフォーマンスが要求される場合。 - 説明
特定の非常に負荷の高い大きな整数の演算 (例えば、暗号処理の一部) を、専用のハードウェアや外部のクラウドサービスにオフロードする方法です。
big.Int を選択する主な理由
- 標準ライブラリの一部
追加の依存関係なしに利用できます。 - 豊富な算術演算とビット演算
加減乗除、剰余、べき乗、ビット操作など、多くの基本的な演算が標準でサポートされています。 - 無限の精度
標準の整数型の範囲を超える非常に大きな整数を正確に扱えます。
- 特定の機能
big.Int
にはない特殊な演算やデータ型が必要な場合があります。 - 精度
浮動小数点数は近似的な表現であり、誤差が生じる可能性があります。高精度な10進数演算には専用のライブラリが適しています。 - パフォーマンス
標準の整数型の方が一般的に高速です。