Golang でランダム値を扱うパッケージは以下の 2 つが用意されています。
- math/rand … 擬似乱数生成器 (pseudo-random number generator)。初期化のための Seed によって生成される一連の乱数が決定するため、再現性がある。高速な生成が可能だが、生成されるランダム値が予測され得るため、暗号系技術での使用には適さない。
- crypto/rand … 暗号論的擬似乱数生成器(CSPRNG: cryptographically secure pseudo random number generator)。生成されるランダム値を予測するのが困難で、暗号系技術での使用に適している。例えば、秘密鍵の生成や、Nonce 値の生成に用いることができる。
math/rand
に比べ、crypto/rand
でのランダム値生成は時間がかかる。
math/rand による乱数生成
math/rand
パッケージのランダム生成器 (*rand.Rand
) は、rand.New
コンストラクタで生成します。
典型的には、現在時刻を元にしたシードを与えて初期化します。
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
val := r.Float64() // 乱数を生成(rand.Rand のメソッド)
あるいは、rand.Seed
関数で、トップレベル関数用のシードを設定することもできます。
こちらの方法を使う場合は、*rand.Rand
インスタンスを生成する必要はありません。
seed := time.Now().UnixNano()
rand.Seed(seed)
val := rand.Float64() // 乱数を生成(rand のトップレベル関数)
math/rand
パッケージは、次のような乱数生成関数を提供しています。
メソッド | 説明 | 戻り値の型 | 値の範囲 |
---|---|---|---|
Int31() | 0 以上の 31 ビット整数 | int32 | 0 〜 2,147,483,647 |
Int31n(n) | 0 以上 n 未満の 31 ビット整数 | int32 | 0 〜 n (n ≧ 1) |
Uint32() | 0 以上の 32 ビット整数 | uint32 | 0 〜 4,294,967,295 |
Int63() | 0 以上の 63 ビット整数 | int64 | 0 〜 9,223,372,036,854,775,807 |
Int63n(n) | 0 以上 n 未満の 63 ビット整数 | int64 | 0 〜 n (n ≧ 1) |
Uint64() | 0 以上の 64 ビット整数 | uint64 | 0 〜 18,446,744,073,709,551,615 |
Int() | 0 以上の整数(少なくとも 32 ビット) | int | 0 〜 システム依存 |
Intn(n) | 0 以上 n 未満の整数(少なくとも 32 ビット) | int | 0 〜 n (n ≧ 1) |
Float32() | 浮動小数点数(float32 型) | float32 | [0.0, 1.0) (0.0 以上 1.0 未満) |
Float64() | 浮動小数点数(float64 型) | float64 | [0.0, 1.0) (0.0 以上 1.0 未満) |
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
fmt.Println(r.Int31()) // 1401608483 (int32)
fmt.Println(r.Uint32()) // 3362137694 (uint32)
fmt.Println("-------")
fmt.Println(r.Int63()) // 9200732467715261966 (int64)
fmt.Println(r.Uint64()) // 17815840155156866386 (uint64)
fmt.Println("-------")
fmt.Println(r.Float32()) // 0.34179267 (float32)
fmt.Println(r.Float64()) // 0.7233553795829966 (float64)
fmt.Println("-------")
fmt.Println(r.Int31n(10000)) // 1451 (int32)
fmt.Println(r.Int63n(10000)) // 7504 (int64)
}
Read(p []byte)
メソッドを使用すると、任意の長さのバイト配列を生成することができます。
bytes := make([]byte, 4)
r.Read(bytes)
fmt.Println(bytes) // [231 52 121 45]
crypto/rand による乱数生成
予測の難しい乱数値が欲しいときは、math/rand
パッケージの代わりに crypto/rand
を使用します。
パッケージ内部の乱数生成には、Linux 系 OS では /dev/urandom
や getentropy(2)
、Windows では RtlGenRandom
API が使われています。
次の例では、rand.Int
関数を使って、0 以上 n 未満のランダム整数 (*big.Int
) を生成しています。
シードによる初期化は必要ありません。
package main
import (
"crypto/rand"
"fmt"
"math/big"
)
func main() {
// 0 以上 n 未満のセキュアなランダム整数を生成
var n int64 = 1_000_000_000
val, err := rand.Int(rand.Reader, big.NewInt(n))
if err != nil {
panic(err)
}
fmt.Println(val.Int64()) //=> 721357881
}
crypto/rand
パッケージは、math/rand
ほど柔軟なランダム生成関数を備えていませんが、encoding/binary
パッケージと組み合わせて使用すると、任意の型にランダム値を詰めることができます。
package main
import (
"crypto/rand"
"encoding/binary"
"fmt"
)
func main() {
var v int32 // 任意の型を指定可能
binary.Read(rand.Reader, binary.LittleEndian, &v)
fmt.Println(v) //=> 2017071203
}
math/rand
と同様、crypto/rand
でも指定した長さのバイト配列を生成することができます。
func main() {
bytes := make([]byte, 4)
rand.Read(bytes)
fmt.Println(bytes) //=> [235 16 197 104]
}
(応用)crypto/rand で math/rand のシードを生成する
現在時刻を math/rand
のシードに使うのは抵抗があるけど、crypto/rand
ほどセキュアな乱数は必要ないという場合は、シードの生成のみを crypto/rand
で行うという方法があります。
package main
import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
)
// 予測困難なシードで math/rand を初期化
func initRandomSeed() {
var seed int64
if err := binary.Read(cryptorand.Reader, binary.LittleEndian, &seed); err != nil {
panic(err)
}
rand.Seed(seed)
}
func main() {
initRandomSeed()
for i := 0; i < 20; i++ {
fmt.Print(rand.Intn(10), " ") // 0 〜 9 のランダム整数を生成
}
fmt.Println()
}
$ go run main.go
9 8 9 7 3 3 0 2 1 4 9 9 2 6 1 6 0 5 8 1
(応用)ランダムな文字列を生成する
次の RandomId
関数は、指定した長さのランダムな文字列を生成します(例: m6t2j7a
)。
package strutil
import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
"math/rand"
"os"
"time"
)
func init() {
initRandomSeed()
}
// 指定した長さのランダム文字列を生成します
func RandomId(length int) string {
const CANDIDATES = "23456789abcdefghijkmnopqrstuvwxyz"
const LEN_CAND = len(CANDIDATES)
bytes := make([]byte, 0, length) // 必要な capacity は明確
for i := 0; i < length; i++ {
bytes = append(bytes, CANDIDATES[rand.Intn(LEN_CAND)])
}
return string(bytes)
}
// 予測困難なシードで math/rand を初期化します
func initRandomSeed() {
var seed int64
if err := binary.Read(cryptorand.Reader, binary.LittleEndian, &seed); err != nil {
fmt.Fprintln(os.Stderr, "WARN: Seed creation by crypto/rand failed, so current time was used instead")
seed = time.Now().UnixNano()
}
rand.Seed(seed)
}
package main
import (
"fmt"
"hello/strutil"
)
func main() {
fmt.Println(strutil.RandomId(7)) //=> "m6t2j7a"
}